/**
* $Id: mxGraph.java,v 1.306 2010-09-20 06:21:37 gaudenz Exp $
* Copyright (c) 2007, Gaudenz Alder
*/
package com.mxgraph.view;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.Element;
import com.mxgraph.canvas.mxGraphics2DCanvas;
import com.mxgraph.canvas.mxICanvas;
import com.mxgraph.canvas.mxImageCanvas;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxGraphModel.Filter;
import com.mxgraph.model.mxGraphModel.mxChildChange;
import com.mxgraph.model.mxGraphModel.mxCollapseChange;
import com.mxgraph.model.mxGraphModel.mxGeometryChange;
import com.mxgraph.model.mxGraphModel.mxRootChange;
import com.mxgraph.model.mxGraphModel.mxStyleChange;
import com.mxgraph.model.mxGraphModel.mxTerminalChange;
import com.mxgraph.model.mxGraphModel.mxValueChange;
import com.mxgraph.model.mxGraphModel.mxVisibleChange;
import com.mxgraph.model.mxICell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.util.mxResources;
import com.mxgraph.util.mxUndoableEdit;
import com.mxgraph.util.mxUndoableEdit.mxUndoableChange;
import com.mxgraph.util.mxUtils;
/**
* Implements a graph object that allows to create diagrams from a graph model
* and stylesheet.
*
* <h3>Images</h3>
* To create an image from a graph, use the following code for a given
* XML document (doc) and File (file):
*
* <code>
* Image img = mxCellRenderer.createBufferedImage(
* graph, null, 1, Color.WHITE, false, null);
* ImageIO.write(img, "png", file);
* </code>
*
* If the XML is given as a string rather than a document, the document can
* be obtained using mxUtils.parse.
*
* This class fires the following events:
*
* mxEvent.ROOT fires if the root in the model has changed. This event has no
* properties.
*
* mxEvent.ALIGN_CELLS fires between begin- and endUpdate in alignCells. The
* <code>cells</code> and <code>align</code> properties contain the respective
* arguments that were passed to alignCells.
*
* mxEvent.FLIP_EDGE fires between begin- and endUpdate in flipEdge. The
* <code>edge</code> property contains the edge passed to flipEdge.
*
* mxEvent.ORDER_CELLS fires between begin- and endUpdate in orderCells. The
* <code>cells</code> and <code>back</code> properties contain the respective
* arguments that were passed to orderCells.
*
* mxEvent.CELLS_ORDERED fires between begin- and endUpdate in cellsOrdered.
* The <code>cells</code> and <code>back</code> arguments contain the
* respective arguments that were passed to cellsOrdered.
*
* mxEvent.GROUP_CELLS fires between begin- and endUpdate in groupCells. The
* <code>group</code>, <code>cells</code> and <code>border</code> arguments
* contain the respective arguments that were passed to groupCells.
*
* mxEvent.UNGROUP_CELLS fires between begin- and endUpdate in ungroupCells.
* The <code>cells</code> property contains the array of cells that was passed
* to ungroupCells.
*
* mxEvent.REMOVE_CELLS_FROM_PARENT fires between begin- and endUpdate in
* removeCellsFromParent. The <code>cells</code> property contains the array of
* cells that was passed to removeCellsFromParent.
*
* mxEvent.ADD_CELLS fires between begin- and endUpdate in addCells. The
* <code>cells</code>, <code>parent</code>, <code>index</code>,
* <code>source</code> and <code>target</code> properties contain the
* respective arguments that were passed to addCells.
*
* mxEvent.CELLS_ADDED fires between begin- and endUpdate in cellsAdded. The
* <code>cells</code>, <code>parent</code>, <code>index</code>,
* <code>source</code>, <code>target</code> and <code>absolute</code>
* properties contain the respective arguments that were passed to cellsAdded.
*
* mxEvent.REMOVE_CELLS fires between begin- and endUpdate in removeCells. The
* <code>cells</code> and <code>includeEdges</code> arguments contain the
* respective arguments that were passed to removeCells.
*
* mxEvent.CELLS_REMOVED fires between begin- and endUpdate in cellsRemoved.
* The <code>cells</code> argument contains the array of cells that was
* removed.
*
* mxEvent.SPLIT_EDGE fires between begin- and endUpdate in splitEdge. The
* <code>edge</code> property contains the edge to be splitted, the
* <code>cells</code>, <code>newEdge</code>, <code>dx</code> and
* <code>dy</code> properties contain the respective arguments that were passed
* to splitEdge.
*
* mxEvent.TOGGLE_CELLS fires between begin- and endUpdate in toggleCells. The
* <code>show</code>, <code>cells</code> and <code>includeEdges</code>
* properties contain the respective arguments that were passed to toggleCells.
*
* mxEvent.FOLD_CELLS fires between begin- and endUpdate in foldCells. The
* <code>collapse</code>, <code>cells</code> and <code>recurse</code>
* properties contain the respective arguments that were passed to foldCells.
*
* mxEvent.CELLS_FOLDED fires between begin- and endUpdate in cellsFolded. The
* <code>collapse</code>, <code>cells</code> and <code>recurse</code>
* properties contain the respective arguments that were passed to cellsFolded.
*
* mxEvent.UPDATE_CELL_SIZE fires between begin- and endUpdate in
* updateCellSize. The <code>cell</code> and <code>ignoreChildren</code>
* properties contain the respective arguments that were passed to
* updateCellSize.
*
* mxEvent.RESIZE_CELLS fires between begin- and endUpdate in resizeCells. The
* <code>cells</code> and <code>bounds</code> properties contain the respective
* arguments that were passed to resizeCells.
*
* mxEvent.CELLS_RESIZED fires between begin- and endUpdate in cellsResized.
* The <code>cells</code> and <code>bounds</code> properties contain the
* respective arguments that were passed to cellsResized.
*
* mxEvent.MOVE_CELLS fires between begin- and endUpdate in moveCells. The
* <code>cells</code>, <code>dx</code>, <code>dy</code>, <code>clone</code>,
* <code>target</code> and <code>location</code> properties contain the
* respective arguments that were passed to moveCells.
*
* mxEvent.CELLS_MOVED fires between begin- and endUpdate in cellsMoved. The
* <code>cells</code>, <code>dx</code>, <code>dy</code> and
* <code>disconnect</code> properties contain the respective arguments that
* were passed to cellsMoved.
*
* mxEvent.CONNECT_CELL fires between begin- and endUpdate in connectCell. The
* <code>edge</code>, <code>terminal</code> and <code>source</code> properties
* contain the respective arguments that were passed to connectCell.
*
* mxEvent.CELL_CONNECTED fires between begin- and endUpdate in cellConnected.
* The <code>edge</code>, <code>terminal</code> and <code>source</code>
* properties contain the respective arguments that were passed to
* cellConnected.
*
* mxEvent.REPAINT fires if a repaint was requested by calling repaint. The
* <code>region</code> property contains the optional mxRectangle that was
* passed to repaint to define the dirty region.
*/
public class mxGraph extends mxEventSource
{
/**
* Adds required resources.
*/
static
{
try
{
mxResources.add("com.mxgraph.resources.graph");
}
catch (Exception e)
{
// ignore
}
}
/**
* Holds the version number of this release. Current version
* is 1.4.2.0.
*/
public static final String VERSION = "1.4.2.0";
/**
*
*/
public interface mxICellVisitor
{
/**
*
* @param vertex
* @param edge
*/
boolean visit(Object vertex, Object edge);
}
/**
* Property change event handling.
*/
protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
this);
/**
* Holds the model that contains the cells to be displayed.
*/
protected mxIGraphModel model;
/**
* Holds the view that caches the cell states.
*/
protected mxGraphView view;
/**
* Holds the stylesheet that defines the appearance of the cells.
*/
protected mxStylesheet stylesheet;
/**
* Holds the <mxGraphSelection> that models the current selection.
*/
protected mxGraphSelectionModel selectionModel;
/**
* Specifies the grid size. Default is 10.
*/
protected int gridSize = 10;
/**
* Specifies if the grid is enabled. Default is true.
*/
protected boolean gridEnabled = true;
/**
* Value returned by getOverlap if isAllowOverlapParent returns
* true for the given cell. getOverlap is used in keepInside if
* isKeepInsideParentOnMove returns true. The value specifies the
* portion of the child which is allowed to overlap the parent.
*/
protected double defaultOverlap = 0.5;
/**
* Specifies the default parent to be used to insert new cells.
* This is used in getDefaultParent. Default is null.
*/
protected Object defaultParent;
/**
* Specifies the alternate edge style to be used if the main control point
* on an edge is being doubleclicked. Default is null.
*/
protected String alternateEdgeStyle;
/**
* Specifies the return value for isEnabled. Default is true.
*/
protected boolean enabled = true;
/**
* Specifies the return value for isCell(s)Locked. Default is false.
*/
protected boolean cellsLocked = false;
/**
* Specifies the return value for isCell(s)Editable. Default is true.
*/
protected boolean cellsEditable = true;
/**
* Specifies the return value for isCell(s)Sizable. Default is true.
*/
protected boolean cellsResizable = true;
/**
* Specifies the return value for isCell(s)Movable. Default is true.
*/
protected boolean cellsMovable = true;
/**
* Specifies the return value for isCell(s)Bendable. Default is true.
*/
protected boolean cellsBendable = true;
/**
* Specifies the return value for isCell(s)Selectable. Default is true.
*/
protected boolean cellsSelectable = true;
/**
* Specifies the return value for isCell(s)Deletable. Default is true.
*/
protected boolean cellsDeletable = true;
/**
* Specifies the return value for isCell(s)Cloneable. Default is true.
*/
protected boolean cellsCloneable = true;
/**
* Specifies the return value for isCellDisconntableFromTerminal. Default
* is true.
*/
protected boolean cellsDisconnectable = true;
/**
* Specifies the return value for isLabel(s)Clipped. Default is false.
*/
protected boolean labelsClipped = false;
/**
* Specifies the return value for edges in isLabelMovable. Default is true.
*/
protected boolean edgeLabelsMovable = true;
/**
* Specifies the return value for vertices in isLabelMovable. Default is false.
*/
protected boolean vertexLabelsMovable = false;
/**
* Specifies the return value for isDropEnabled. Default is true.
*/
protected boolean dropEnabled = true;
/**
* Specifies if dropping onto edges should be enabled. Default is true.
*/
protected boolean splitEnabled = true;
/**
* Specifies if the graph should automatically update the cell size
* after an edit. This is used in isAutoSizeCell. Default is false.
*/
protected boolean autoSizeCells = false;
/**
* <mxRectangle> that specifies the area in which all cells in the
* diagram should be placed. Uses in getMaximumGraphBounds. Use a width
* or height of 0 if you only want to give a upper, left corner.
*/
protected mxRectangle maximumGraphBounds = null;
/**
* mxRectangle that specifies the minimum size of the graph canvas inside
* the scrollpane.
*/
protected mxRectangle minimumGraphSize = null;
/**
* Border to be added to the bottom and right side when the container is
* being resized after the graph has been changed. Default is 0.
*/
protected int border = 0;
/**
* Specifies if edges should appear in the foreground regardless of their
* order in the model. This has precendence over keepEdgeInBackground
* Default is false.
*/
protected boolean keepEdgesInForeground = false;
/**
* Specifies if edges should appear in the background regardless of their
* order in the model. Default is false.
*/
protected boolean keepEdgesInBackground = false;
/**
* Specifies if the cell size should be changed to the preferred size when
* a cell is first collapsed. Default is true.
*/
protected boolean collapseToPreferredSize = true;
/**
* Specifies if negative coordinates for vertices are allowed. Default is true.
*/
protected boolean allowNegativeCoordinates = true;
/**
* Specifies the return value for isConstrainChildren. Default is true.
*/
protected boolean constrainChildren = true;
/**
* Specifies if a parent should contain the child bounds after a resize of
* the child. Default is true.
*/
protected boolean extendParents = true;
/**
* Specifies if parents should be extended according to the <extendParents>
* switch if cells are added. Default is true.
*/
protected boolean extendParentsOnAdd = true;
/**
* Specifies if the scale and translate should be reset if
* the root changes in the model. Default is true.
*/
protected boolean resetViewOnRootChange = true;
/**
* Specifies if loops (aka self-references) are allowed.
* Default is false.
*/
protected boolean resetEdgesOnResize = false;
/**
* Specifies if edge control points should be reset after
* the move of a connected cell. Default is false.
*/
protected boolean resetEdgesOnMove = false;
/**
* Specifies if edge control points should be reset after
* the the edge has been reconnected. Default is true.
*/
protected boolean resetEdgesOnConnect = true;
/**
* Specifies if loops (aka self-references) are allowed.
* Default is false.
*/
protected boolean allowLoops = false;
/**
* Specifies the multiplicities to be used for validation of the graph.
*/
protected mxMultiplicity[] multiplicities;
/**
* Specifies the default style for loops.
*/
protected mxEdgeStyle.mxEdgeStyleFunction defaultLoopStyle = mxEdgeStyle.Loop;
/**
* Specifies if multiple edges in the same direction between
* the same pair of vertices are allowed. Default is true.
*/
protected boolean multigraph = true;
/**
* Specifies if edges are connectable. Default is false.
* This overrides the connectable field in edges.
*/
protected boolean connectableEdges = false;
/**
* Specifies if edges with disconnected terminals are
* allowed in the graph. Default is false.
*/
protected boolean allowDanglingEdges = true;
/**
* Specifies if edges that are cloned should be validated and only inserted
* if they are valid. Default is true.
*/
protected boolean cloneInvalidEdges = false;
/**
* Specifies if edges should be disconnected from their terminals when they
* are moved. Default is true.
*/
protected boolean disconnectOnMove = true;
/**
* Specifies if labels should be visible. This is used in
* getLabel. Default is true.
*/
protected boolean labelsVisible = true;
/**
* Specifies the return value for isHtmlLabel. Default is false.
*/
protected boolean htmlLabels = false;
/**
* Specifies if nesting of swimlanes is allowed. Default is true.
*/
protected boolean swimlaneNesting = true;
/**
* Specifies the maximum number of changes that should be processed to find
* the dirty region. If the number of changes is larger, then the complete
* grah is repainted. A value of zero will always compute the dirty region
* for any number of changes. Default is 1000.
*/
protected int changesRepaintThreshold = 1000;
/**
* Specifies if the origin should be automatically updated.
*/
protected boolean autoOrigin = false;
/**
* Holds the current automatic origin.
*/
protected mxPoint origin = new mxPoint();
/**
* Fires repaint events for full repaints.
*/
protected mxIEventListener fullRepaintHandler = new mxIEventListener()
{
public void invoke(Object sender, mxEventObject evt)
{
repaint();
}
};
/**
* Fires repaint events for model changes.
*/
protected mxIEventListener graphModelChangeHandler = new mxIEventListener()
{
@SuppressWarnings("unchecked")
public void invoke(Object sender, mxEventObject evt)
{
mxRectangle dirty = graphModelChanged((mxIGraphModel) sender,
(List<mxUndoableChange>) ((mxUndoableEdit) evt
.getProperty("edit")).getChanges());
repaint(dirty);
}
};
/**
* Constructs a new graph with an empty
* {@link com.mxgraph.model.mxGraphModel}.
*/
public mxGraph()
{
this(null, null);
}
/**
* Constructs a new graph for the specified model. If no model is
* specified, then a new, empty {@link com.mxgraph.model.mxGraphModel} is
* used.
*
* @param model Model that contains the graph data
*/
public mxGraph(mxIGraphModel model)
{
this(model, null);
}
/**
* Constructs a new graph for the specified model. If no model is
* specified, then a new, empty {@link com.mxgraph.model.mxGraphModel} is
* used.
*
* @param stylesheet The stylesheet to use for the graph.
*/
public mxGraph(mxStylesheet stylesheet)
{
this(null, stylesheet);
}
/**
* Constructs a new graph for the specified model. If no model is
* specified, then a new, empty {@link com.mxgraph.model.mxGraphModel} is
* used.
*
* @param model Model that contains the graph data
*/
public mxGraph(mxIGraphModel model, mxStylesheet stylesheet)
{
selectionModel = createSelectionModel();
setModel((model != null) ? model : new mxGraphModel());
setStylesheet((stylesheet != null) ? stylesheet : createStylesheet());
setView(createGraphView());
}
/**
* Constructs a new selection model to be used in this graph.
*/
protected mxGraphSelectionModel createSelectionModel()
{
return new mxGraphSelectionModel(this);
}
/**
* Constructs a new stylesheet to be used in this graph.
*/
protected mxStylesheet createStylesheet()
{
return new mxStylesheet();
}
/**
* Constructs a new view to be used in this graph.
*/
protected mxGraphView createGraphView()
{
return new mxGraphView(this);
}
/**
* Returns the graph model that contains the graph data.
*
* @return Returns the model that contains the graph data
*/
public mxIGraphModel getModel()
{
return model;
}
/**
* Sets the graph model that contains the data, and fires an
* mxEvent.CHANGE followed by an mxEvent.REPAINT event.
*
* @param value Model that contains the graph data
*/
public void setModel(mxIGraphModel value)
{
if (model != null)
{
model.removeListener(graphModelChangeHandler);
}
Object oldModel = model;
model = value;
if (view != null)
{
view.revalidate();
}
model.addListener(mxEvent.CHANGE, graphModelChangeHandler);
changeSupport.firePropertyChange("model", oldModel, model);
repaint();
}
/**
* Returns the view that contains the cell states.
*
* @return Returns the view that contains the cell states
*/
public mxGraphView getView()
{
return view;
}
/**
* Sets the view that contains the cell states.
*
* @param value View that contains the cell states
*/
public void setView(mxGraphView value)
{
if (view != null)
{
view.removeListener(fullRepaintHandler);
}
Object oldView = view;
view = value;
if (view != null)
{
view.revalidate();
}
// Listens to changes in the view
view.addListener(mxEvent.SCALE, fullRepaintHandler);
view.addListener(mxEvent.TRANSLATE, fullRepaintHandler);
view.addListener(mxEvent.SCALE_AND_TRANSLATE, fullRepaintHandler);
view.addListener(mxEvent.UP, fullRepaintHandler);
view.addListener(mxEvent.DOWN, fullRepaintHandler);
changeSupport.firePropertyChange("view", oldView, view);
}
/**
* Returns the stylesheet that provides the style.
*
* @return Returns the stylesheet that provides the style.
*/
public mxStylesheet getStylesheet()
{
return stylesheet;
}
/**
* Sets the stylesheet that provides the style.
*
* @param value Stylesheet that provides the style.
*/
public void setStylesheet(mxStylesheet value)
{
mxStylesheet oldValue = stylesheet;
stylesheet = value;
changeSupport.firePropertyChange("stylesheet", oldValue, stylesheet);
}
/**
* Returns the cells to be selected for the given list of changes.
*/
public Object[] getSelectionCellsForChanges(List<mxUndoableChange> changes)
{
List<Object> cells = new ArrayList<Object>();
Iterator<mxUndoableChange> it = changes.iterator();
while (it.hasNext())
{
Object change = it.next();
if (change instanceof mxChildChange)
{
cells.add(((mxChildChange) change).getChild());
}
else if (change instanceof mxTerminalChange)
{
cells.add(((mxTerminalChange) change).getCell());
}
else if (change instanceof mxValueChange)
{
cells.add(((mxValueChange) change).getCell());
}
else if (change instanceof mxStyleChange)
{
cells.add(((mxStyleChange) change).getCell());
}
else if (change instanceof mxGeometryChange)
{
cells.add(((mxGeometryChange) change).getCell());
}
else if (change instanceof mxCollapseChange)
{
cells.add(((mxCollapseChange) change).getCell());
}
else if (change instanceof mxVisibleChange)
{
mxVisibleChange vc = (mxVisibleChange) change;
if (vc.isVisible())
{
cells.add(((mxVisibleChange) change).getCell());
}
}
}
return mxGraphModel.getTopmostCells(model, cells.toArray());
}
/**
* Called when the graph model changes. Invokes processChange on each
* item of the given array to update the view accordingly.
*/
public mxRectangle graphModelChanged(mxIGraphModel sender,
List<mxUndoableChange> changes)
{
int thresh = getChangesRepaintThreshold();
boolean ignoreDirty = thresh > 0 && changes.size() > thresh;
// Ignores dirty rectangle if there was a root change
if (!ignoreDirty)
{
Iterator<mxUndoableChange> it = changes.iterator();
while (it.hasNext())
{
if (it.next() instanceof mxRootChange)
{
ignoreDirty = true;
break;
}
}
}
mxRectangle dirty = processChanges(changes, true, ignoreDirty);
view.validate();
if (isAutoOrigin())
{
updateOrigin();
}
if (!ignoreDirty)
{
mxRectangle tmp = processChanges(changes, false, ignoreDirty);
if (tmp != null)
{
if (dirty == null)
{
dirty = tmp;
}
else
{
dirty.add(tmp);
}
}
}
removeSelectionCells(getRemovedCellsForChanges(changes));
return dirty;
}
/**
* Extends the canvas by doing another validation with a shifted
* global translation if the bounds of the graph are below (0,0).
*
* The first validation is required to compute the bounds of the graph
* while the second validation is required to apply the new translate.
*/
protected void updateOrigin()
{
mxRectangle bounds = getGraphBounds();
if (bounds != null)
{
double scale = getView().getScale();
double x = bounds.getX() / scale - getBorder();
double y = bounds.getY() / scale - getBorder();
if (x < 0 || y < 0)
{
double x0 = Math.min(0, x);
double y0 = Math.min(0, y);
origin.setX(origin.getX() + x0);
origin.setY(origin.getY() + y0);
mxPoint t = getView().getTranslate();
getView().setTranslate(
new mxPoint(t.getX() - x0, t.getY() - y0));
}
else if ((x > 0 || y > 0)
&& (origin.getX() < 0 || origin.getY() < 0))
{
double dx = Math.min(-origin.getX(), x);
double dy = Math.min(-origin.getY(), y);
origin.setX(origin.getX() + dx);
origin.setY(origin.getY() + dy);
mxPoint t = getView().getTranslate();
getView().setTranslate(
new mxPoint(t.getX() - dx, t.getY() - dy));
}
}
}
/**
* Returns the cells that have been removed from the model.
*/
public Object[] getRemovedCellsForChanges(List<mxUndoableChange> changes)
{
List<Object> result = new ArrayList<Object>();
Iterator<mxUndoableChange> it = changes.iterator();
while (it.hasNext())
{
Object change = it.next();
if (change instanceof mxRootChange)
{
break;
}
else if (change instanceof mxChildChange)
{
mxChildChange cc = (mxChildChange) change;
if (cc.getParent() == null)
{
result.addAll(mxGraphModel.getDescendants(model,
cc.getChild()));
}
}
else if (change instanceof mxVisibleChange)
{
Object cell = ((mxVisibleChange) change).getCell();
result.addAll(mxGraphModel.getDescendants(model, cell));
}
}
return result.toArray();
}
/**
* Processes the changes and returns the minimal rectangle to be
* repainted in the buffer. A return value of null means no repaint
* is required.
*/
public mxRectangle processChanges(List<mxUndoableChange> changes,
boolean invalidate, boolean ignoreDirty)
{
mxRectangle bounds = null;
Iterator<mxUndoableChange> it = changes.iterator();
while (it.hasNext())
{
mxRectangle rect = processChange(it.next(), invalidate, ignoreDirty);
if (bounds == null)
{
bounds = rect;
}
else
{
bounds.add(rect);
}
}
return bounds;
}
/**
* Processes the given change and invalidates the respective cached data
* in <view>. This fires a <root> event if the root has changed in the
* model.
*/
public mxRectangle processChange(mxUndoableChange change,
boolean invalidate, boolean ignoreDirty)
{
mxRectangle result = null;
if (change instanceof mxRootChange)
{
result = (ignoreDirty) ? null : getGraphBounds();
if (invalidate)
{
clearSelection();
removeStateForCell(((mxRootChange) change).getPrevious());
if (isResetViewOnRootChange())
{
view.setEventsEnabled(false);
try
{
view.scaleAndTranslate(1, 0, 0);
}
finally
{
view.setEventsEnabled(true);
}
}
}
fireEvent(new mxEventObject(mxEvent.ROOT));
}
else if (change instanceof mxChildChange)
{
mxChildChange cc = (mxChildChange) change;
// Repaints the parent area if it is a rendered cell (vertex or
// edge) otherwise only the child area is repainted, same holds
// if the parent and previous are the same object, in which case
// only the child area needs to be repainted (change of order)
if (!ignoreDirty)
{
if (cc.getParent() != cc.getPrevious())
{
if (model.isVertex(cc.getParent())
|| model.isEdge(cc.getParent()))
{
result = getBoundingBox(cc.getParent(), true, true);
}
if (model.isVertex(cc.getPrevious())
|| model.isEdge(cc.getPrevious()))
{
if (result != null)
{
result.add(getBoundingBox(cc.getPrevious(), true,
true));
}
else
{
result = getBoundingBox(cc.getPrevious(), true,
true);
}
}
}
if (result == null)
{
result = getBoundingBox(cc.getChild(), true, true);
}
}
if (invalidate)
{
if (cc.getParent() != null)
{
view.clear(cc.getChild(), false, true);
}
else
{
removeStateForCell(cc.getChild());
}
}
}
else if (change instanceof mxTerminalChange)
{
Object cell = ((mxTerminalChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(cell, true);
}
if (invalidate)
{
view.invalidate(cell);
}
}
else if (change instanceof mxValueChange)
{
Object cell = ((mxValueChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(cell);
}
if (invalidate)
{
view.clear(cell, false, false);
}
}
else if (change instanceof mxStyleChange)
{
Object cell = ((mxStyleChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(cell, true);
}
if (invalidate)
{
// TODO: Add includeEdges argument to clear method for
// not having to call invalidate in this case (where it
// is possible that the perimeter has changed, which
// means the connected edges need to be invalidated)
view.clear(cell, false, false);
view.invalidate(cell);
}
}
else if (change instanceof mxGeometryChange)
{
Object cell = ((mxGeometryChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(cell, true, true);
}
if (invalidate)
{
view.invalidate(cell);
}
}
else if (change instanceof mxCollapseChange)
{
Object cell = ((mxCollapseChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(((mxCollapseChange) change).getCell(),
true, true);
}
if (invalidate)
{
removeStateForCell(cell);
}
}
else if (change instanceof mxVisibleChange)
{
Object cell = ((mxVisibleChange) change).getCell();
if (!ignoreDirty)
{
result = getBoundingBox(((mxVisibleChange) change).getCell(),
true, true);
}
if (invalidate)
{
removeStateForCell(cell);
}
}
return result;
}
/**
* Removes all cached information for the given cell and its descendants.
* This is called when a cell was removed from the model.
*
* @param cell Cell that was removed from the model.
*/
protected void removeStateForCell(Object cell)
{
int childCount = model.getChildCount(cell);
for (int i = 0; i < childCount; i++)
{
removeStateForCell(model.getChildAt(cell, i));
}
view.removeState(cell);
}
//
// Cell styles
//
/**
* Returns an array of key, value pairs representing the cell style for the
* given cell. If no string is defined in the model that specifies the
* style, then the default style for the cell is returned or <EMPTY_ARRAY>,
* if not style can be found.
*
* @param cell Cell whose style should be returned.
* @return Returns the style of the cell.
*/
public Map<String, Object> getCellStyle(Object cell)
{
Map<String, Object> style = (model.isEdge(cell)) ? stylesheet
.getDefaultEdgeStyle() : stylesheet.getDefaultVertexStyle();
String name = model.getStyle(cell);
if (name != null)
{
style = stylesheet.getCellStyle(name, style);
}
if (style == null)
{
style = mxStylesheet.EMPTY_STYLE;
}
return style;
}
/**
* Sets the style of the selection cells to the given value.
*
* @param style String representing the new style of the cells.
*/
public Object[] setCellStyle(String style)
{
return setCellStyle(style, null);
}
/**
* Sets the style of the specified cells. If no cells are given, then the
* selection cells are changed.
*
* @param style String representing the new style of the cells.
* @param cells Optional array of <mxCells> to set the style for. Default is the
* selection cells.
*/
public Object[] setCellStyle(String style, Object[] cells)
{
if (cells == null)
{
cells = getSelectionCells();
}
if (cells != null)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
model.setStyle(cells[i], style);
}
}
finally
{
model.endUpdate();
}
}
return cells;
}
/**
* Toggles the boolean value for the given key in the style of the
* given cell. If no cell is specified then the selection cell is
* used.
*
* @param key Key for the boolean value to be toggled.
* @param defaultValue Default boolean value if no value is defined.
* @param cell Cell whose style should be modified.
*/
public Object toggleCellStyle(String key, boolean defaultValue, Object cell)
{
return toggleCellStyles(key, defaultValue, new Object[] { cell })[0];
}
/**
* Toggles the boolean value for the given key in the style of the
* selection cells.
*
* @param key Key for the boolean value to be toggled.
* @param defaultValue Default boolean value if no value is defined.
*/
public Object[] toggleCellStyles(String key, boolean defaultValue)
{
return toggleCellStyles(key, defaultValue, null);
}
/**
* Toggles the boolean value for the given key in the style of the given
* cells. If no cells are specified, then the selection cells are used. For
* example, this can be used to toggle mxConstants.STYLE_ROUNDED or any
* other style with a boolean value.
*
* @param key String representing the key of the boolean style to be toggled.
* @param defaultValue Default boolean value if no value is defined.
* @param cells Cells whose styles should be modified.
*/
public Object[] toggleCellStyles(String key, boolean defaultValue,
Object[] cells)
{
if (cells == null)
{
cells = getSelectionCells();
}
if (cells != null && cells.length > 0)
{
mxCellState state = view.getState(cells[0]);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(cells[0]);
if (style != null)
{
String value = (mxUtils.isTrue(style, key, defaultValue)) ? "0"
: "1";
setCellStyles(key, value, cells);
}
}
return cells;
}
/**
* Sets the key to value in the styles of the selection cells.
*
* @param key String representing the key to be assigned.
* @param value String representing the new value for the key.
*/
public Object[] setCellStyles(String key, String value)
{
return setCellStyles(key, value, null);
}
/**
* Sets the key to value in the styles of the given cells. This will modify
* the existing cell styles in-place and override any existing assignment
* for the given key. If no cells are specified, then the selection cells
* are changed. If no value is specified, then the respective key is
* removed from the styles.
*
* @param key String representing the key to be assigned.
* @param value String representing the new value for the key.
* @param cells Array of cells to change the style for.
*/
public Object[] setCellStyles(String key, String value, Object[] cells)
{
if (cells == null)
{
cells = getSelectionCells();
}
mxUtils.setCellStyles(model, cells, key, value);
return cells;
}
/**
* Toggles the given bit for the given key in the styles of the selection
* cells.
*
* @param key String representing the key to toggle the flag in.
* @param flag Integer that represents the bit to be toggled.
*/
public Object[] toggleCellStyleFlags(String key, int flag)
{
return toggleCellStyleFlags(key, flag, null);
}
/**
* Toggles the given bit for the given key in the styles of the specified
* cells.
*
* @param key String representing the key to toggle the flag in.
* @param flag Integer that represents the bit to be toggled.
* @param cells Optional array of <mxCells> to change the style for. Default is
* the selection cells.
*/
public Object[] toggleCellStyleFlags(String key, int flag, Object[] cells)
{
return setCellStyleFlags(key, flag, null, cells);
}
/**
* Sets or toggles the given bit for the given key in the styles of the
* selection cells.
*
* @param key String representing the key to toggle the flag in.
* @param flag Integer that represents the bit to be toggled.
* @param value Boolean value to be used or null if the value should be
* toggled.
*/
public Object[] setCellStyleFlags(String key, int flag, boolean value)
{
return setCellStyleFlags(key, flag, value, null);
}
/**
* Sets or toggles the given bit for the given key in the styles of the
* specified cells.
*
* @param key String representing the key to toggle the flag in.
* @param flag Integer that represents the bit to be toggled.
* @param value Boolean value to be used or null if the value should be
* toggled.
* @param cells Optional array of cells to change the style for. If no
* cells are specified then the selection cells are used.
*/
public Object[] setCellStyleFlags(String key, int flag, Boolean value,
Object[] cells)
{
if (cells == null)
{
cells = getSelectionCells();
}
if (cells != null && cells.length > 0)
{
if (value == null)
{
mxCellState state = view.getState(cells[0]);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(cells[0]);
if (style != null)
{
int current = mxUtils.getInt(style, key);
value = !((current & flag) == flag);
}
}
mxUtils.setCellStyleFlags(model, cells, key, flag, value);
}
return cells;
}
//
// Cell alignment and orientation
//
/**
* Aligns the selection cells vertically or horizontally according to the
* given alignment.
*
* @param align Specifies the alignment. Possible values are all constants
* in mxConstants with an ALIGN prefix.
*/
public Object[] alignCells(String align)
{
return alignCells(align, null);
}
/**
* Aligns the given cells vertically or horizontally according to the given
* alignment.
*
* @param align Specifies the alignment. Possible values are all constants
* in mxConstants with an ALIGN prefix.
* @param cells Array of cells to be aligned.
*/
public Object[] alignCells(String align, Object[] cells)
{
return alignCells(align, cells, null);
}
/**
* Aligns the given cells vertically or horizontally according to the given
* alignment using the optional parameter as the coordinate.
*
* @param align Specifies the alignment. Possible values are all constants
* in mxConstants with an ALIGN prefix.
* @param cells Array of cells to be aligned.
* @param param Optional coordinate for the alignment.
*/
public Object[] alignCells(String align, Object[] cells, Object param)
{
if (cells == null)
{
cells = getSelectionCells();
}
if (cells != null && cells.length > 1)
{
// Finds the required coordinate for the alignment
if (param == null)
{
for (int i = 0; i < cells.length; i++)
{
mxGeometry geo = getCellGeometry(cells[i]);
if (geo != null && !model.isEdge(cells[i]))
{
if (param == null)
{
if (align == null
|| align.equals(mxConstants.ALIGN_LEFT))
{
param = geo.getX();
}
else if (align.equals(mxConstants.ALIGN_CENTER))
{
param = geo.getX() + geo.getWidth() / 2;
break;
}
else if (align.equals(mxConstants.ALIGN_RIGHT))
{
param = geo.getX() + geo.getWidth();
}
else if (align.equals(mxConstants.ALIGN_TOP))
{
param = geo.getY();
}
else if (align.equals(mxConstants.ALIGN_MIDDLE))
{
param = geo.getY() + geo.getHeight() / 2;
break;
}
else if (align.equals(mxConstants.ALIGN_BOTTOM))
{
param = geo.getY() + geo.getHeight();
}
}
else
{
double tmp = Double.parseDouble(String
.valueOf(param));
if (align == null
|| align.equals(mxConstants.ALIGN_LEFT))
{
param = Math.min(tmp, geo.getX());
}
else if (align.equals(mxConstants.ALIGN_RIGHT))
{
param = Math.max(tmp,
geo.getX() + geo.getWidth());
}
else if (align.equals(mxConstants.ALIGN_TOP))
{
param = Math.min(tmp, geo.getY());
}
else if (align.equals(mxConstants.ALIGN_BOTTOM))
{
param = Math.max(tmp,
geo.getY() + geo.getHeight());
}
}
}
}
}
// Aligns the cells to the coordinate
model.beginUpdate();
try
{
double tmp = Double.parseDouble(String.valueOf(param));
for (int i = 0; i < cells.length; i++)
{
mxGeometry geo = getCellGeometry(cells[i]);
if (geo != null && !model.isEdge(cells[i]))
{
geo = (mxGeometry) geo.clone();
if (align == null
|| align.equals(mxConstants.ALIGN_LEFT))
{
geo.setX(tmp);
}
else if (align.equals(mxConstants.ALIGN_CENTER))
{
geo.setX(tmp - geo.getWidth() / 2);
}
else if (align.equals(mxConstants.ALIGN_RIGHT))
{
geo.setX(tmp - geo.getWidth());
}
else if (align.equals(mxConstants.ALIGN_TOP))
{
geo.setY(tmp);
}
else if (align.equals(mxConstants.ALIGN_MIDDLE))
{
geo.setY(tmp - geo.getHeight() / 2);
}
else if (align.equals(mxConstants.ALIGN_BOTTOM))
{
geo.setY(tmp - geo.getHeight());
}
model.setGeometry(cells[i], geo);
if (isResetEdgesOnMove())
{
resetEdges(new Object[] { cells[i] });
}
}
}
fireEvent(new mxEventObject(mxEvent.ALIGN_CELLS, "cells",
cells, "align", align));
}
finally
{
model.endUpdate();
}
}
return cells;
}
/**
* Called when the main control point of the edge is double-clicked. This
* implementation switches between null (default) and alternateEdgeStyle
* and resets the edges control points. Finally, a flip event is fired
* before endUpdate is called on the model.
*
* @param edge Cell that represents the edge to be flipped.
* @return Returns the edge that has been flipped.
*/
public Object flipEdge(Object edge)
{
if (edge != null && alternateEdgeStyle != null)
{
model.beginUpdate();
try
{
String style = model.getStyle(edge);
if (style == null || style.length() == 0)
{
model.setStyle(edge, alternateEdgeStyle);
}
else
{
model.setStyle(edge, null);
}
// Removes all existing control points
resetEdge(edge);
fireEvent(new mxEventObject(mxEvent.FLIP_EDGE, "edge", edge));
}
finally
{
model.endUpdate();
}
}
return edge;
}
//
// Order
//
/**
* Moves the selection cells to the front or back. This is a shortcut method.
*
* @param back Specifies if the cells should be moved to back.
*/
public Object[] orderCells(boolean back)
{
return orderCells(back, null);
}
/**
* Moves the given cells to the front or back. The change is carried out
* using cellsOrdered. This method fires mxEvent.ORDER_CELLS while the
* transaction is in progress.
*
* @param back Specifies if the cells should be moved to back.
* @param cells Array of cells whose order should be changed. If null is
* specified then the selection cells are used.
*/
public Object[] orderCells(boolean back, Object[] cells)
{
if (cells == null)
{
cells = mxUtils.sortCells(getSelectionCells(), true);
}
model.beginUpdate();
try
{
cellsOrdered(cells, back);
fireEvent(new mxEventObject(mxEvent.ORDER_CELLS, "cells", cells,
"back", back));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Moves the given cells to the front or back. This method fires
* mxEvent.CELLS_ORDERED while the transaction is in progress.
*
* @param cells Array of cells whose order should be changed.
* @param back Specifies if the cells should be moved to back.
*/
public void cellsOrdered(Object[] cells, boolean back)
{
if (cells != null)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
Object parent = model.getParent(cells[i]);
if (back)
{
model.add(parent, cells[i], i);
}
else
{
model.add(parent, cells[i],
model.getChildCount(parent) - 1);
}
}
fireEvent(new mxEventObject(mxEvent.CELLS_ORDERED, "cells",
cells, "back", back));
}
finally
{
model.endUpdate();
}
}
}
//
// Grouping
//
/**
* Groups the selection cells. This is a shortcut method.
*
* @return Returns the new group.
*/
public Object groupCells()
{
return groupCells(null);
}
/**
* Groups the selection cells and adds them to the given group. This is a
* shortcut method.
*
* @return Returns the new group.
*/
public Object groupCells(Object group)
{
return groupCells(group, 0);
}
/**
* Groups the selection cells and adds them to the given group. This is a
* shortcut method.
*
* @return Returns the new group.
*/
public Object groupCells(Object group, double border)
{
return groupCells(group, border, null);
}
/**
* Adds the cells into the given group. The change is carried out using
* cellsAdded, cellsMoved and cellsResized. This method fires
* mxEvent.GROUP_CELLS while the transaction is in progress. Returns the
* new group. A group is only created if there is at least one entry in the
* given array of cells.
*
* @param group Cell that represents the target group. If null is specified
* then a new group is created using createGroupCell.
* @param border Integer that specifies the border between the child area
* and the group bounds.
* @param cells Optional array of cells to be grouped. If null is specified
* then the selection cells are used.
*/
public Object groupCells(Object group, double border, Object[] cells)
{
if (cells == null)
{
cells = mxUtils.sortCells(getSelectionCells(), true);
}
cells = getCellsForGroup(cells);
if (group == null)
{
group = createGroupCell(cells);
}
mxRectangle bounds = getBoundsForGroup(group, cells, border);
if (cells.length > 0 && bounds != null)
{
Object parent = model.getParent(cells[0]);
model.beginUpdate();
try
{
// Checks if the group has a geometry and
// creates one if one does not exist
if (getCellGeometry(group) == null)
{
model.setGeometry(group, new mxGeometry());
}
// Adds the children into the group and moves
int index = model.getChildCount(group);
cellsAdded(cells, group, index, null, null, false);
cellsMoved(cells, -bounds.getX(), -bounds.getY(), false, true);
// Adds the group into the parent and resizes
index = model.getChildCount(parent);
cellsAdded(new Object[] { group }, parent, index, null, null,
false);
cellsResized(new Object[] { group },
new mxRectangle[] { bounds });
fireEvent(new mxEventObject(mxEvent.GROUP_CELLS, "group",
group, "cells", cells, "border", border));
}
finally
{
model.endUpdate();
}
}
return group;
}
/**
* Returns the cells with the same parent as the first cell
* in the given array.
*/
public Object[] getCellsForGroup(Object[] cells)
{
List<Object> result = new ArrayList<Object>(cells.length);
if (cells != null && cells.length > 0)
{
Object parent = model.getParent(cells[0]);
result.add(cells[0]);
// Filters selection cells with the same parent
for (int i = 1; i < cells.length; i++)
{
if (model.getParent(cells[i]) == parent)
{
result.add(cells[i]);
}
}
}
return result.toArray();
}
/**
* Returns the bounds to be used for the given group and children. This
* implementation computes the bounding box of the geometries of all
* vertices in the given children array. Edges are ignored. If the group
* cell is a swimlane the title region is added to the bounds.
*/
public mxRectangle getBoundsForGroup(Object group, Object[] children,
double border)
{
mxRectangle result = getBoundingBoxFromGeometry(children);
if (result != null)
{
if (isSwimlane(group))
{
mxRectangle size = getStartSize(group);
result.setX(result.getX() - size.getWidth());
result.setY(result.getY() - size.getHeight());
result.setWidth(result.getWidth() + size.getWidth());
result.setHeight(result.getHeight() + size.getHeight());
}
// Adds the border
result.setX(result.getX() - border);
result.setY(result.getY() - border);
result.setWidth(result.getWidth() + 2 * border);
result.setHeight(result.getHeight() + 2 * border);
}
return result;
}
/**
* Hook for creating the group cell to hold the given array of <mxCells> if
* no group cell was given to the <group> function. The children are just
* for informational purpose, they will be added to the returned group
* later. Note that the returned group should have a geometry. The
* coordinates of which are later overridden.
*
* @param cells
* @return Returns a new group cell.
*/
public Object createGroupCell(Object[] cells)
{
mxCell group = new mxCell("", new mxGeometry(), null);
group.setVertex(true);
group.setConnectable(false);
return group;
}
/**
* Ungroups the selection cells. This is a shortcut method.
*/
public Object[] ungroupCells()
{
return ungroupCells(null);
}
/**
* Ungroups the given cells by moving the children the children to their
* parents parent and removing the empty groups.
*
* @param cells Array of cells to be ungrouped. If null is specified then
* the selection cells are used.
* @return Returns the children that have been removed from the groups.
*/
public Object[] ungroupCells(Object[] cells)
{
List<Object> result = new ArrayList<Object>();
if (cells == null)
{
cells = getSelectionCells();
// Finds the cells with children
List<Object> tmp = new ArrayList<Object>(cells.length);
for (int i = 0; i < cells.length; i++)
{
if (model.getChildCount(cells[i]) > 0)
{
tmp.add(cells[i]);
}
}
cells = tmp.toArray();
}
if (cells != null && cells.length > 0)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
Object[] children = mxGraphModel.getChildren(model,
cells[i]);
if (children != null && children.length > 0)
{
Object parent = model.getParent(cells[i]);
int index = model.getChildCount(parent);
cellsAdded(children, parent, index, null, null, true);
result.addAll(Arrays.asList(children));
}
}
cellsRemoved(addAllEdges(cells));
fireEvent(new mxEventObject(mxEvent.UNGROUP_CELLS, "cells",
cells));
}
finally
{
model.endUpdate();
}
}
return result.toArray();
}
/**
* Removes the selection cells from their parents and adds them to the
* default parent returned by getDefaultParent.
*/
public Object[] removeCellsFromParent()
{
return removeCellsFromParent(null);
}
/**
* Removes the specified cells from their parents and adds them to the
* default parent.
*
* @param cells Array of cells to be removed from their parents.
* @return Returns the cells that were removed from their parents.
*/
public Object[] removeCellsFromParent(Object[] cells)
{
if (cells == null)
{
cells = getSelectionCells();
}
model.beginUpdate();
try
{
Object parent = getDefaultParent();
int index = model.getChildCount(parent);
cellsAdded(cells, parent, index, null, null, true);
fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS_FROM_PARENT,
"cells", cells));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*/
public Object[] updateGroupBounds()
{
return updateGroupBounds(null);
}
/**
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
* @param cells The groups whose bounds should be updated.
*/
public Object[] updateGroupBounds(Object[] cells)
{
return updateGroupBounds(cells, 0);
}
/**
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
* @param cells The groups whose bounds should be updated.
* @param border The border to be added in the group.
*/
public Object[] updateGroupBounds(Object[] cells, int border)
{
return updateGroupBounds(cells, border, false);
}
/**
* Updates the bounds of the given array of groups so that it includes
* all child vertices.
*
* @param cells The groups whose bounds should be updated.
* @param border The border to be added in the group.
* @param moveParent Specifies if the group should be moved.
*/
public Object[] updateGroupBounds(Object[] cells, int border,
boolean moveParent)
{
if (cells == null)
{
cells = getSelectionCells();
}
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
mxGeometry geo = getCellGeometry(cells[i]);
if (geo != null)
{
Object[] children = getChildCells(cells[i]);
if (children != null && children.length > 0)
{
mxRectangle childBounds = getBoundingBoxFromGeometry(children);
if (childBounds.getWidth() > 0
&& childBounds.getHeight() > 0)
{
mxRectangle size = (isSwimlane(cells[i])) ? getStartSize(cells[i])
: new mxRectangle();
geo = (mxGeometry) geo.clone();
if (moveParent)
{
geo.setX(geo.getX() + childBounds.getX()
- size.getWidth() - border);
geo.setY(geo.getY() + childBounds.getY()
- size.getHeight() - border);
}
geo.setWidth(childBounds.getWidth()
+ size.getWidth() + 2 * border);
geo.setHeight(childBounds.getHeight()
+ size.getHeight() + 2 * border);
model.setGeometry(cells[i], geo);
moveCells(children,
-childBounds.getX() + size.getWidth()
+ border, -childBounds.getY()
+ size.getHeight() + border);
}
}
}
}
}
finally
{
model.endUpdate();
}
return cells;
}
//
// Cell cloning, insertion and removal
//
/**
* Clones all cells in the given array. To clone all children in a cell and
* add them to another graph:
*
* <code>
* graph2.addCells(graph.cloneCells(new Object[] { parent }));
* </code>
*/
public Object[] cloneCells(Object[] cells)
{
return cloneCells(cells, true);
}
/**
* Returns the clones for the given cells. If the terminal of an edge is
* not in the given array, then the respective end is assigned a terminal
* point and the terminal is removed. If a cloned edge is invalid and
* allowInvalidEdges is false, then a null pointer will be at this position
* in the returned array. Use getCloneableCells on the input array to only
* clone the cells where isCellCloneable returns true.
*
* @param cells Array of mxCells to be cloned.
* @return Returns the clones of the given cells.
*/
public Object[] cloneCells(Object[] cells, boolean allowInvalidEdges)
{
Object[] clones = null;
if (cells != null)
{
Collection<Object> tmp = new LinkedHashSet<Object>(cells.length);
tmp.addAll(Arrays.asList(cells));
if (!tmp.isEmpty())
{
double scale = view.getScale();
mxPoint trans = view.getTranslate();
clones = model.cloneCells(cells, true);
for (int i = 0; i < cells.length; i++)
{
if (!allowInvalidEdges
&& model.isEdge(clones[i])
&& getEdgeValidationError(clones[i],
model.getTerminal(clones[i], true),
model.getTerminal(clones[i], false)) != null)
{
clones[i] = null;
}
else
{
mxGeometry g = model.getGeometry(clones[i]);
if (g != null)
{
mxCellState state = view.getState(cells[i]);
mxCellState pstate = view.getState(model
.getParent(cells[i]));
if (state != null && pstate != null)
{
double dx = pstate.getOrigin().getX();
double dy = pstate.getOrigin().getY();
if (model.isEdge(clones[i]))
{
// Checks if the source is cloned or sets the terminal point
Object src = model.getTerminal(cells[i],
true);
while (src != null && !tmp.contains(src))
{
src = model.getParent(src);
}
if (src == null)
{
mxPoint pt = state.getAbsolutePoint(0);
g.setTerminalPoint(
new mxPoint(pt.getX() / scale
- trans.getX(), pt
.getY()
/ scale
- trans.getY()), true);
}
// Checks if the target is cloned or sets the terminal point
Object trg = model.getTerminal(cells[i],
false);
while (trg != null && !tmp.contains(trg))
{
trg = model.getParent(trg);
}
if (trg == null)
{
mxPoint pt = state
.getAbsolutePoint(state
.getAbsolutePointCount() - 1);
g.setTerminalPoint(
new mxPoint(pt.getX() / scale
- trans.getX(), pt
.getY()
/ scale
- trans.getY()), false);
}
// Translates the control points
List<mxPoint> points = g.getPoints();
if (points != null)
{
Iterator<mxPoint> it = points
.iterator();
while (it.hasNext())
{
mxPoint pt = it.next();
pt.setX(pt.getX() + dx);
pt.setY(pt.getY() + dy);
}
}
}
else
{
g.setX(g.getX() + dx);
g.setY(g.getY() + dy);
}
}
}
}
}
}
else
{
clones = new Object[] {};
}
}
return clones;
}
/**
* Creates and adds a new vertex with an empty style.
*/
public Object insertVertex(Object parent, String id, Object value,
double x, double y, double width, double height)
{
return insertVertex(parent, id, value, x, y, width, height, null);
}
/**
* Adds a new vertex into the given parent using value as the user object
* and the given coordinates as the geometry of the new vertex. The id and
* style are used for the respective properties of the new cell, which is
* returned.
*
* @param parent Cell that specifies the parent of the new vertex.
* @param id Optional string that defines the Id of the new vertex.
* @param value Object to be used as the user object.
* @param x Integer that defines the x coordinate of the vertex.
* @param y Integer that defines the y coordinate of the vertex.
* @param width Integer that defines the width of the vertex.
* @param height Integer that defines the height of the vertex.
* @param style Optional string that defines the cell style.
* @return Returns the new vertex that has been inserted.
*/
public Object insertVertex(Object parent, String id, Object value,
double x, double y, double width, double height, String style)
{
Object vertex = createVertex(parent, id, value, x, y, width, height,
style);
return addCell(vertex, parent);
}
/**
* Hook method that creates the new vertex for insertVertex.
*
* @param parent Cell that specifies the parent of the new vertex.
* @param id Optional string that defines the Id of the new vertex.
* @param value Object to be used as the user object.
* @param x Integer that defines the x coordinate of the vertex.
* @param y Integer that defines the y coordinate of the vertex.
* @param width Integer that defines the width of the vertex.
* @param height Integer that defines the height of the vertex.
* @param style Optional string that defines the cell style.
* @return Returns the new vertex to be inserted.
*/
public Object createVertex(Object parent, String id, Object value,
double x, double y, double width, double height, String style)
{
mxGeometry geometry = new mxGeometry(x, y, width, height);
mxCell vertex = new mxCell(value, geometry, style);
vertex.setId(id);
vertex.setVertex(true);
vertex.setConnectable(true);
return vertex;
}
/**
* Creates and adds a new edge with an empty style.
*/
public Object insertEdge(Object parent, String id, Object value,
Object source, Object target)
{
return insertEdge(parent, id, value, source, target, null);
}
/**
* Adds a new edge into the given parent using value as the user object and
* the given source and target as the terminals of the new edge. The Id and
* style are used for the respective properties of the new cell, which is
* returned.
*
* @param parent Cell that specifies the parent of the new edge.
* @param id Optional string that defines the Id of the new edge.
* @param value Object to be used as the user object.
* @param source Cell that defines the source of the edge.
* @param target Cell that defines the target of the edge.
* @param style Optional string that defines the cell style.
* @return Returns the new edge that has been inserted.
*/
public Object insertEdge(Object parent, String id, Object value,
Object source, Object target, String style)
{
Object edge = createEdge(parent, id, value, source, target, style);
return addEdge(edge, parent, source, target, null);
}
/**
* Hook method that creates the new edge for insertEdge. This
* implementation does not set the source and target of the edge, these
* are set when the edge is added to the model.
*
* @param parent Cell that specifies the parent of the new edge.
* @param id Optional string that defines the Id of the new edge.
* @param value Object to be used as the user object.
* @param source Cell that defines the source of the edge.
* @param target Cell that defines the target of the edge.
* @param style Optional string that defines the cell style.
* @return Returns the new edge to be inserted.
*/
public Object createEdge(Object parent, String id, Object value,
Object source, Object target, String style)
{
mxCell edge = new mxCell(value, new mxGeometry(), style);
edge.setId(id);
edge.setEdge(true);
edge.getGeometry().setRelative(true);
return edge;
}
/**
* Adds the edge to the parent and connects it to the given source and
* target terminals. This is a shortcut method.
*
* @param edge Edge to be inserted into the given parent.
* @param parent Object that represents the new parent. If no parent is
* given then the default parent is used.
* @param source Optional cell that represents the source terminal.
* @param target Optional cell that represents the target terminal.
* @param index Optional index to insert the cells at. Default is to append.
* @return Returns the edge that was added.
*/
public Object addEdge(Object edge, Object parent, Object source,
Object target, Integer index)
{
return addCell(edge, parent, index, source, target);
}
/**
* Adds the cell to the default parent. This is a shortcut method.
*
* @param cell Cell to be inserted.
* @return Returns the cell that was added.
*/
public Object addCell(Object cell)
{
return addCell(cell, null);
}
/**
* Adds the cell to the parent. This is a shortcut method.
*
* @param cell Cell tobe inserted.
* @param parent Object that represents the new parent. If no parent is
* given then the default parent is used.
* @return Returns the cell that was added.
*/
public Object addCell(Object cell, Object parent)
{
return addCell(cell, parent, null, null, null);
}
/**
* Adds the cell to the parent and connects it to the given source and
* target terminals. This is a shortcut method.
*
* @param cell Cell to be inserted into the given parent.
* @param parent Object that represents the new parent. If no parent is
* given then the default parent is used.
* @param index Optional index to insert the cells at. Default is to append.
* @param source Optional cell that represents the source terminal.
* @param target Optional cell that represents the target terminal.
* @return Returns the cell that was added.
*/
public Object addCell(Object cell, Object parent, Integer index,
Object source, Object target)
{
return addCells(new Object[] { cell }, parent, index, source, target)[0];
}
/**
* Adds the cells to the default parent. This is a shortcut method.
*
* @param cells Array of cells to be inserted.
* @return Returns the cells that were added.
*/
public Object[] addCells(Object[] cells)
{
return addCells(cells, null);
}
/**
* Adds the cells to the parent. This is a shortcut method.
*
* @param cells Array of cells to be inserted.
* @param parent Optional cell that represents the new parent. If no parent
* is specified then the default parent is used.
* @return Returns the cells that were added.
*/
public Object[] addCells(Object[] cells, Object parent)
{
return addCells(cells, parent, null);
}
/**
* Adds the cells to the parent at the given index. This is a shortcut method.
*
* @param cells Array of cells to be inserted.
* @param parent Optional cell that represents the new parent. If no parent
* is specified then the default parent is used.
* @param index Optional index to insert the cells at. Default is to append.
* @return Returns the cells that were added.
*/
public Object[] addCells(Object[] cells, Object parent, Integer index)
{
return addCells(cells, parent, index, null, null);
}
/**
* Adds the cells to the parent at the given index, connecting each cell to
* the optional source and target terminal. The change is carried out using
* cellsAdded. This method fires mxEvent.ADD_CELLS while the transaction
* is in progress.
*
* @param cells Array of cells to be added.
* @param parent Optional cell that represents the new parent. If no parent
* is specified then the default parent is used.
* @param index Optional index to insert the cells at. Default is to append.
* @param source Optional source terminal for all inserted cells.
* @param target Optional target terminal for all inserted cells.
* @return Returns the cells that were added.
*/
public Object[] addCells(Object[] cells, Object parent, Integer index,
Object source, Object target)
{
if (parent == null)
{
parent = getDefaultParent();
}
if (index == null)
{
index = model.getChildCount(parent);
}
model.beginUpdate();
try
{
cellsAdded(cells, parent, index, source, target, false);
fireEvent(new mxEventObject(mxEvent.ADD_CELLS, "cells", cells,
"parent", parent, "index", index, "source", source,
"target", target));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Adds the specified cells to the given parent. This method fires
* mxEvent.CELLS_ADDED while the transaction is in progress.
*/
public void cellsAdded(Object[] cells, Object parent, Integer index,
Object source, Object target, boolean absolute)
{
if (cells != null && parent != null && index != null)
{
model.beginUpdate();
try
{
mxCellState parentState = (absolute) ? view.getState(parent)
: null;
mxPoint o1 = (parentState != null) ? parentState.getOrigin()
: null;
mxPoint zero = new mxPoint(0, 0);
for (int i = 0; i < cells.length; i++)
{
Object previous = model.getParent(cells[i]);
// Keeps the cell at its absolute location
if (o1 != null && cells[i] != parent && parent != previous)
{
mxCellState oldState = view.getState(previous);
mxPoint o2 = (oldState != null) ? oldState.getOrigin()
: zero;
mxGeometry geo = model.getGeometry(cells[i]);
if (geo != null)
{
double dx = o2.getX() - o1.getX();
double dy = o2.getY() - o1.getY();
geo = (mxGeometry) geo.clone();
geo.translate(dx, dy);
if (!geo.isRelative() && model.isVertex(cells[i])
&& !isAllowNegativeCoordinates())
{
geo.setX(Math.max(0, geo.getX()));
geo.setY(Math.max(0, geo.getY()));
}
model.setGeometry(cells[i], geo);
}
}
// Decrements all following indices
// if cell is already in parent
if (parent == previous)
{
index--;
}
model.add(parent, cells[i], index + i);
// Extends the parent
if (isExtendParentsOnAdd() && isExtendParent(cells[i]))
{
extendParent(cells[i]);
}
// Constrains the child
constrainChild(cells[i]);
// Sets the source terminal
if (source != null)
{
cellConnected(cells[i], source, true, null);
}
// Sets the target terminal
if (target != null)
{
cellConnected(cells[i], target, false, null);
}
}
fireEvent(new mxEventObject(mxEvent.CELLS_ADDED, "cells",
cells, "parent", parent, "index", index, "source",
source, "target", target, "absolute", absolute));
}
finally
{
model.endUpdate();
}
}
}
/**
* Removes the selection cells from the graph.
*
* @return Returns the cells that have been removed.
*/
public Object[] removeCells()
{
return removeCells(null);
}
/**
* Removes the given cells from the graph.
*
* @param cells Array of cells to remove.
* @return Returns the cells that have been removed.
*/
public Object[] removeCells(Object[] cells)
{
return removeCells(cells, true);
}
/**
* Removes the given cells from the graph including all connected edges if
* includeEdges is true. The change is carried out using cellsRemoved. This
* method fires mxEvent.REMOVE_CELLS while the transaction is in progress.
*
* @param cells Array of cells to remove. If null is specified then the
* selection cells which are deletable are used.
* @param includeEdges Specifies if all connected edges should be removed as
* well.
*/
public Object[] removeCells(Object[] cells, boolean includeEdges)
{
if (cells == null)
{
cells = getDeletableCells(getSelectionCells());
}
// Adds all edges to the cells
if (includeEdges)
{
cells = getDeletableCells(addAllEdges(cells));
}
model.beginUpdate();
try
{
cellsRemoved(cells);
fireEvent(new mxEventObject(mxEvent.REMOVE_CELLS, "cells", cells,
"includeEdges", includeEdges));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Removes the given cells from the model. This method fires
* mxEvent.CELLS_REMOVED while the transaction is in progress.
*
* @param cells Array of cells to remove.
*/
public void cellsRemoved(Object[] cells)
{
if (cells != null && cells.length > 0)
{
double scale = view.getScale();
mxPoint tr = view.getTranslate();
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
// Disconnects edges which are not in cells
Collection<Object> cellSet = new HashSet<Object>();
cellSet.addAll(Arrays.asList(cells));
Object[] edges = getConnections(cells[i]);
for (int j = 0; j < edges.length; j++)
{
if (!cellSet.contains(edges[j]))
{
mxGeometry geo = model.getGeometry(edges[j]);
if (geo != null)
{
mxCellState state = view.getState(edges[j]);
if (state != null)
{
geo = (mxGeometry) geo.clone();
boolean source = view.getVisibleTerminal(
edges[j], true) == cells[i];
int n = (source) ? 0 : state
.getAbsolutePointCount() - 1;
mxPoint pt = state.getAbsolutePoint(n);
geo.setTerminalPoint(new mxPoint(pt.getX()
/ scale - tr.getX(), pt.getY()
/ scale - tr.getY()), source);
model.setTerminal(edges[j], null, source);
model.setGeometry(edges[j], geo);
}
}
}
}
model.remove(cells[i]);
}
fireEvent(new mxEventObject(mxEvent.CELLS_REMOVED, "cells",
cells));
}
finally
{
model.endUpdate();
}
}
}
/**
*
*/
public Object splitEdge(Object edge, Object[] cells)
{
return splitEdge(edge, cells, null, 0, 0);
}
/**
*
*/
public Object splitEdge(Object edge, Object[] cells, double dx, double dy)
{
return splitEdge(edge, cells, null, dx, dy);
}
/**
* Splits the given edge by adding a newEdge between the previous source
* and the given cell and reconnecting the source of the given edge to the
* given cell. Fires mxEvent.SPLIT_EDGE while the transaction is in
* progress.
*
* @param edge Object that represents the edge to be splitted.
* @param cells Array that contains the cells to insert into the edge.
* @param newEdge Object that represents the edge to be inserted.
* @return Returns the new edge that has been inserted.
*/
public Object splitEdge(Object edge, Object[] cells, Object newEdge,
double dx, double dy)
{
if (newEdge == null)
{
newEdge = cloneCells(new Object[] { edge })[0];
}
Object parent = model.getParent(edge);
Object source = model.getTerminal(edge, true);
model.beginUpdate();
try
{
cellsMoved(cells, dx, dy, false, false);
cellsAdded(cells, parent, model.getChildCount(parent), null, null,
true);
cellsAdded(new Object[] { newEdge }, parent,
model.getChildCount(parent), source, cells[0], false);
cellConnected(edge, cells[0], true, null);
fireEvent(new mxEventObject(mxEvent.SPLIT_EDGE, "edge", edge,
"cells", cells, "newEdge", newEdge, "dx", dx, "dy", dy));
}
finally
{
model.endUpdate();
}
return newEdge;
}
//
// Cell visibility
//
/**
* Sets the visible state of the selection cells. This is a shortcut
* method.
*
* @param show Boolean that specifies the visible state to be assigned.
* @return Returns the cells whose visible state was changed.
*/
public Object[] toggleCells(boolean show)
{
return toggleCells(show, null);
}
/**
* Sets the visible state of the specified cells. This is a shortcut
* method.
*
* @param show Boolean that specifies the visible state to be assigned.
* @param cells Array of cells whose visible state should be changed.
* @return Returns the cells whose visible state was changed.
*/
public Object[] toggleCells(boolean show, Object[] cells)
{
return toggleCells(show, cells, true);
}
/**
* Sets the visible state of the specified cells and all connected edges
* if includeEdges is true. The change is carried out using cellsToggled.
* This method fires mxEvent.TOGGLE_CELLS while the transaction is in
* progress.
*
* @param show Boolean that specifies the visible state to be assigned.
* @param cells Array of cells whose visible state should be changed. If
* null is specified then the selection cells are used.
* @return Returns the cells whose visible state was changed.
*/
public Object[] toggleCells(boolean show, Object[] cells,
boolean includeEdges)
{
if (cells == null)
{
cells = getSelectionCells();
}
// Adds all connected edges recursively
if (includeEdges)
{
cells = addAllEdges(cells);
}
model.beginUpdate();
try
{
cellsToggled(cells, show);
fireEvent(new mxEventObject(mxEvent.TOGGLE_CELLS, "show", show,
"cells", cells, "includeEdges", includeEdges));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Sets the visible state of the specified cells.
*
* @param cells Array of cells whose visible state should be changed.
* @param show Boolean that specifies the visible state to be assigned.
*/
public void cellsToggled(Object[] cells, boolean show)
{
if (cells != null && cells.length > 0)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
model.setVisible(cells[i], show);
}
}
finally
{
model.endUpdate();
}
}
}
//
// Folding
//
/**
* Sets the collapsed state of the selection cells without recursion.
* This is a shortcut method.
*
* @param collapse Boolean that specifies the collapsed state to be
* assigned.
* @return Returns the cells whose collapsed state was changed.
*/
public Object[] foldCells(boolean collapse)
{
return foldCells(collapse, false);
}
/**
* Sets the collapsed state of the selection cells. This is a shortcut
* method.
*
* @param collapse Boolean that specifies the collapsed state to be
* assigned.
* @param recurse Boolean that specifies if the collapsed state should
* be assigned to all descendants.
* @return Returns the cells whose collapsed state was changed.
*/
public Object[] foldCells(boolean collapse, boolean recurse)
{
return foldCells(collapse, recurse, null);
}
/**
* Sets the collapsed state of the specified cells and all descendants
* if recurse is true. The change is carried out using cellsFolded.
* This method fires mxEvent.FOLD_CELLS while the transaction is in
* progress. Returns the cells whose collapsed state was changed.
*
* @param collapse Boolean indicating the collapsed state to be assigned.
* @param recurse Boolean indicating if the collapsed state of all
* descendants should be set.
* @param cells Array of cells whose collapsed state should be set. If
* null is specified then the foldable selection cells are used.
*/
public Object[] foldCells(boolean collapse, boolean recurse, Object[] cells)
{
if (cells == null)
{
cells = getFoldableCells(getSelectionCells(), collapse);
}
model.beginUpdate();
try
{
cellsFolded(cells, collapse, recurse);
fireEvent(new mxEventObject(mxEvent.FOLD_CELLS, "cells", cells,
"collapse", collapse, "recurse", recurse));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Sets the collapsed state of the specified cells. This method fires
* mxEvent.CELLS_FOLDED while the transaction is in progress. Returns the
* cells whose collapsed state was changed.
*
* @param cells Array of cells whose collapsed state should be set.
* @param collapse Boolean indicating the collapsed state to be assigned.
* @param recurse Boolean indicating if the collapsed state of all
* descendants should be set.
*/
public void cellsFolded(Object[] cells, boolean collapse, boolean recurse)
{
if (cells != null && cells.length > 0)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
if (collapse != isCellCollapsed(cells[i]))
{
model.setCollapsed(cells[i], collapse);
swapBounds(cells[i], collapse);
if (isExtendParent(cells[i]))
{
extendParent(cells[i]);
}
if (recurse)
{
Object[] children = mxGraphModel.getChildren(model,
cells[i]);
cellsFolded(children, collapse, recurse);
}
}
}
fireEvent(new mxEventObject(mxEvent.CELLS_FOLDED, "cells",
cells, "collapse", collapse, "recurse", recurse));
}
finally
{
model.endUpdate();
}
}
}
/**
* Swaps the alternate and the actual bounds in the geometry of the given
* cell invoking updateAlternateBounds before carrying out the swap.
*
* @param cell Cell for which the bounds should be swapped.
* @param willCollapse Boolean indicating if the cell is going to be collapsed.
*/
public void swapBounds(Object cell, boolean willCollapse)
{
if (cell != null)
{
mxGeometry geo = model.getGeometry(cell);
if (geo != null)
{
geo = (mxGeometry) geo.clone();
updateAlternateBounds(cell, geo, willCollapse);
geo.swap();
model.setGeometry(cell, geo);
}
}
}
/**
* Updates or sets the alternate bounds in the given geometry for the given
* cell depending on whether the cell is going to be collapsed. If no
* alternate bounds are defined in the geometry and
* collapseToPreferredSize is true, then the preferred size is used for
* the alternate bounds. The top, left corner is always kept at the same
* location.
*
* @param cell Cell for which the geometry is being udpated.
* @param geo Geometry for which the alternate bounds should be updated.
* @param willCollapse Boolean indicating if the cell is going to be collapsed.
*/
public void updateAlternateBounds(Object cell, mxGeometry geo,
boolean willCollapse)
{
if (cell != null && geo != null)
{
if (geo.getAlternateBounds() == null)
{
mxRectangle bounds = null;
if (isCollapseToPreferredSize())
{
bounds = getPreferredSizeForCell(cell);
if (isSwimlane(cell))
{
mxRectangle size = getStartSize(cell);
bounds.setHeight(Math.max(bounds.getHeight(),
size.getHeight()));
bounds.setWidth(Math.max(bounds.getWidth(),
size.getWidth()));
}
}
if (bounds == null)
{
bounds = geo;
}
geo.setAlternateBounds(new mxRectangle(geo.getX(), geo.getY(),
bounds.getWidth(), bounds.getHeight()));
}
else
{
geo.getAlternateBounds().setX(geo.getX());
geo.getAlternateBounds().setY(geo.getY());
}
}
}
/**
* Returns an array with the given cells and all edges that are connected
* to a cell or one of its descendants.
*/
public Object[] addAllEdges(Object[] cells)
{
List<Object> allCells = new ArrayList<Object>(cells.length);
allCells.addAll(Arrays.asList(cells));
allCells.addAll(Arrays.asList(getAllEdges(cells)));
return allCells.toArray();
}
/**
* Returns all edges connected to the given cells or their descendants.
*/
public Object[] getAllEdges(Object[] cells)
{
List<Object> edges = new ArrayList<Object>();
if (cells != null)
{
for (int i = 0; i < cells.length; i++)
{
int edgeCount = model.getEdgeCount(cells[i]);
for (int j = 0; j < edgeCount; j++)
{
edges.add(model.getEdgeAt(cells[i], j));
}
// Recurses
Object[] children = mxGraphModel.getChildren(model, cells[i]);
edges.addAll(Arrays.asList(getAllEdges(children)));
}
}
return edges.toArray();
}
//
// Cell sizing
//
/**
* Updates the size of the given cell in the model using
* getPreferredSizeForCell to get the new size. This function
* fires beforeUpdateSize and afterUpdateSize events.
*
* @param cell <mxCell> for which the size should be changed.
*/
public Object updateCellSize(Object cell)
{
return updateCellSize(cell, false);
}
/**
* Updates the size of the given cell in the model using
* getPreferredSizeForCell to get the new size. This function
* fires mxEvent.UPDATE_CELL_SIZE.
*
* @param cell Cell for which the size should be changed.
*/
public Object updateCellSize(Object cell, boolean ignoreChildren)
{
model.beginUpdate();
try
{
cellSizeUpdated(cell, ignoreChildren);
fireEvent(new mxEventObject(mxEvent.UPDATE_CELL_SIZE, "cell", cell,
"ignoreChildren", ignoreChildren));
}
finally
{
model.endUpdate();
}
return cell;
}
/**
* Updates the size of the given cell in the model using
* getPreferredSizeForCell to get the new size.
*
* @param cell Cell for which the size should be changed.
*/
public void cellSizeUpdated(Object cell, boolean ignoreChildren)
{
if (cell != null)
{
model.beginUpdate();
try
{
mxRectangle size = getPreferredSizeForCell(cell);
mxGeometry geo = model.getGeometry(cell);
if (size != null && geo != null)
{
boolean collapsed = isCellCollapsed(cell);
geo = (mxGeometry) geo.clone();
if (isSwimlane(cell))
{
mxCellState state = view.getState(cell);
Map<String, Object> style = (state != null) ? state
.getStyle() : getCellStyle(cell);
String cellStyle = model.getStyle(cell);
if (cellStyle == null)
{
cellStyle = "";
}
if (mxUtils.isTrue(style, mxConstants.STYLE_HORIZONTAL,
true))
{
cellStyle = mxUtils.setStyle(cellStyle,
mxConstants.STYLE_STARTSIZE,
String.valueOf(size.getHeight() + 8));
if (collapsed)
{
geo.setHeight(size.getHeight() + 8);
}
geo.setWidth(size.getWidth());
}
else
{
cellStyle = mxUtils.setStyle(cellStyle,
mxConstants.STYLE_STARTSIZE,
String.valueOf(size.getWidth() + 8));
if (collapsed)
{
geo.setWidth(size.getWidth() + 8);
}
geo.setHeight(size.getHeight());
}
model.setStyle(cell, cellStyle);
}
else
{
geo.setWidth(size.getWidth());
geo.setHeight(size.getHeight());
}
if (!ignoreChildren && !collapsed)
{
mxRectangle bounds = view.getBounds(mxGraphModel
.getChildren(model, cell));
if (bounds != null)
{
mxPoint tr = view.getTranslate();
double scale = view.getScale();
double width = (bounds.getX() + bounds.getWidth())
/ scale - geo.getX() - tr.getX();
double height = (bounds.getY() + bounds.getHeight())
/ scale - geo.getY() - tr.getY();
geo.setWidth(Math.max(geo.getWidth(), width));
geo.setHeight(Math.max(geo.getHeight(), height));
}
}
cellsResized(new Object[] { cell },
new mxRectangle[] { geo });
}
}
finally
{
model.endUpdate();
}
}
}
/**
* Returns the preferred width and height of the given <mxCell> as an
* <mxRectangle>.
*
* @param cell <mxCell> for which the preferred size should be returned.
*/
public mxRectangle getPreferredSizeForCell(Object cell)
{
mxRectangle result = null;
if (cell != null)
{
mxCellState state = view.getState(cell);
Map<String, Object> style = (state != null) ? state.style
: getCellStyle(cell);
if (style != null && !model.isEdge(cell))
{
double dx = 0;
double dy = 0;
// Adds dimension of image if shape is a label
if (getImage(state) != null
|| mxUtils.getString(style, mxConstants.STYLE_IMAGE) != null)
{
if (mxUtils.getString(style, mxConstants.STYLE_SHAPE, "")
.equals(mxConstants.SHAPE_LABEL))
{
if (mxUtils.getString(style,
mxConstants.STYLE_VERTICAL_ALIGN, "").equals(
mxConstants.ALIGN_MIDDLE))
{
dx += mxUtils.getDouble(style,
mxConstants.STYLE_IMAGE_WIDTH,
mxConstants.DEFAULT_IMAGESIZE);
}
if (mxUtils.getString(style, mxConstants.STYLE_ALIGN,
"").equals(mxConstants.ALIGN_CENTER))
{
dy += mxUtils.getDouble(style,
mxConstants.STYLE_IMAGE_HEIGHT,
mxConstants.DEFAULT_IMAGESIZE);
}
}
}
// Adds spacings
double spacing = mxUtils.getDouble(style,
mxConstants.STYLE_SPACING);
dx += 2 * spacing;
dx += mxUtils.getDouble(style, mxConstants.STYLE_SPACING_LEFT);
dx += mxUtils.getDouble(style, mxConstants.STYLE_SPACING_RIGHT);
dy += 2 * spacing;
dy += mxUtils.getDouble(style, mxConstants.STYLE_SPACING_TOP);
dy += mxUtils
.getDouble(style, mxConstants.STYLE_SPACING_BOTTOM);
// LATER: Add space for collapse/expand icon if applicable
// Adds space for label
String value = getLabel(cell);
if (value != null && value.length() > 0)
{
// FIXME: Check word-wrap style and balance width/height
mxRectangle size = mxUtils.getLabelSize(value, style,
isHtmlLabel(cell), 0);
double width = size.getWidth() + dx;
double height = size.getHeight() + dy;
if (!mxUtils.isTrue(style, mxConstants.STYLE_HORIZONTAL,
true))
{
double tmp = height;
height = width;
width = tmp;
}
if (gridEnabled)
{
width = snap(width + gridSize / 2);
height = snap(height + gridSize / 2);
}
result = new mxRectangle(0, 0, width, height);
}
else
{
double gs2 = 4 * gridSize;
result = new mxRectangle(0, 0, gs2, gs2);
}
}
}
return result;
}
/**
* Sets the bounds of the given cell using resizeCells. Returns the
* cell which was passed to the function.
*
* @param cell <mxCell> whose bounds should be changed.
* @param bounds <mxRectangle> that represents the new bounds.
*/
public Object resizeCell(Object cell, mxRectangle bounds)
{
return resizeCells(new Object[] { cell }, new mxRectangle[] { bounds })[0];
}
/**
* Sets the bounds of the given cells and fires a mxEvent.RESIZE_CELLS
* event. while the transaction is in progress. Returns the cells which
* have been passed to the function.
*
* @param cells Array of cells whose bounds should be changed.
* @param bounds Array of rectangles that represents the new bounds.
*/
public Object[] resizeCells(Object[] cells, mxRectangle[] bounds)
{
model.beginUpdate();
try
{
cellsResized(cells, bounds);
fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS, "cells", cells,
"bounds", bounds));
}
finally
{
model.endUpdate();
}
return cells;
}
/**
* Sets the bounds of the given cells and fires a <mxEvent.CELLS_RESIZED>
* event. If extendParents is true, then the parent is extended if a child
* size is changed so that it overlaps with the parent.
*
* @param cells Array of <mxCells> whose bounds should be changed.
* @param bounds Array of <mxRectangles> that represents the new bounds.
*/
public void cellsResized(Object[] cells, mxRectangle[] bounds)
{
if (cells != null && bounds != null && cells.length == bounds.length)
{
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
mxRectangle tmp = bounds[i];
mxGeometry geo = model.getGeometry(cells[i]);
if (geo != null
&& (geo.getX() != tmp.getX()
|| geo.getY() != tmp.getY()
|| geo.getWidth() != tmp.getWidth() || geo
.getHeight() != tmp.getHeight()))
{
geo = (mxGeometry) geo.clone();
if (geo.isRelative())
{
mxPoint offset = geo.getOffset();
if (offset != null)
{
offset.setX(offset.getX() + tmp.getX()
- geo.getX());
offset.setY(offset.getY() + tmp.getY()
- geo.getY());
}
}
else
{
geo.setX(tmp.getX());
geo.setY(tmp.getY());
}
geo.setWidth(tmp.getWidth());
geo.setHeight(tmp.getHeight());
if (!geo.isRelative() && model.isVertex(cells[i])
&& !isAllowNegativeCoordinates())
{
geo.setX(Math.max(0, geo.getX()));
geo.setY(Math.max(0, geo.getY()));
}
model.setGeometry(cells[i], geo);
if (isExtendParent(cells[i]))
{
extendParent(cells[i]);
}
}
}
if (isResetEdgesOnResize())
{
resetEdges(cells);
}
// RENAME BOUNDSARRAY TO BOUNDS
fireEvent(new mxEventObject(mxEvent.CELLS_RESIZED, "cells",
cells, "bounds", bounds));
}
finally
{
model.endUpdate();
}
}
}
/**
* Resizes the parents recursively so that they contain the complete area
* of the resized child cell.
*
* @param cell <mxCell> that has been resized.
*/
public void extendParent(Object cell)
{
if (cell != null)
{
Object parent = model.getParent(cell);
mxGeometry p = model.getGeometry(parent);
if (parent != null && p != null && !isCellCollapsed(parent))
{
mxGeometry geo = model.getGeometry(cell);
if (geo != null
&& (p.getWidth() < geo.getX() + geo.getWidth() || p
.getHeight() < geo.getY() + geo.getHeight()))
{
p = (mxGeometry) p.clone();
p.setWidth(Math.max(p.getWidth(),
geo.getX() + geo.getWidth()));
p.setHeight(Math.max(p.getHeight(),
geo.getY() + geo.getHeight()));
cellsResized(new Object[] { parent },
new mxRectangle[] { p });
}
}
}
}
//
// Cell moving
//
/**
* Moves the cells by the given amount. This is a shortcut method.
*/
public Object[] moveCells(Object[] cells, double dx, double dy)
{
return moveCells(cells, dx, dy, false);
}
/**
* Moves or clones the cells and moves the cells or clones by the given
* amount. This is a shortcut method.
*/
public Object[] moveCells(Object[] cells, double dx, double dy,
boolean clone)
{
return moveCells(cells, dx, dy, clone, null, null);
}
/**
* Moves or clones the specified cells and moves the cells or clones by the
* given amount, adding them to the optional target cell. The location is
* the position of the mouse pointer as the mouse was released. The change
* is carried out using cellsMoved. This method fires mxEvent.MOVE_CELLS
* while the transaction is in progress.
*
* @param cells Array of cells to be moved, cloned or added to the target.
* @param dx Integer that specifies the x-coordinate of the vector.
* @param dy Integer that specifies the y-coordinate of the vector.
* @param clone Boolean indicating if the cells should be cloned.
* @param target Cell that represents the new parent of the cells.
* @param location Location where the mouse was released.
* @return Returns the cells that were moved.
*/
public Object[] moveCells(Object[] cells, double dx, double dy,
boolean clone, Object target, Point location)
{
if (cells != null && (dx != 0 || dy != 0 || clone || target != null))
{
model.beginUpdate();
try
{
if (clone)
{
cells = cloneCells(cells, isCloneInvalidEdges());
if (target == null)
{
target = getDefaultParent();
}
}
cellsMoved(cells, dx, dy, !clone && isDisconnectOnMove()
&& isAllowDanglingEdges(), target == null);
if (target != null)
{
Integer index = model.getChildCount(target);
cellsAdded(cells, target, index, null, null, true);
}
fireEvent(new mxEventObject(mxEvent.MOVE_CELLS, "cells", cells,
"dx", dx, "dy", dy, "clone", clone, "target", target,
"location", location));
}
finally
{
model.endUpdate();
}
}
return cells;
}
/**
* Moves the specified cells by the given vector, disconnecting the cells
* using disconnectGraph if disconnect is true. This method fires
* mxEvent.CELLS_MOVED while the transaction is in progress.
*/
public void cellsMoved(Object[] cells, double dx, double dy,
boolean disconnect, boolean constrain)
{
if (cells != null && (dx != 0 || dy != 0))
{
model.beginUpdate();
try
{
if (disconnect)
{
disconnectGraph(cells);
}
for (int i = 0; i < cells.length; i++)
{
translateCell(cells[i], dx, dy);
if (constrain)
{
constrainChild(cells[i]);
}
}
if (isResetEdgesOnMove())
{
resetEdges(cells);
}
fireEvent(new mxEventObject(mxEvent.CELLS_MOVED, "cells",
cells, "dx", dx, "dy", dy, "disconnect", disconnect));
}
finally
{
model.endUpdate();
}
}
}
/**
* Translates the geometry of the given cell and stores the new,
* translated geometry in the model as an atomic change.
*/
public void translateCell(Object cell, double dx, double dy)
{
mxGeometry geo = model.getGeometry(cell);
if (geo != null)
{
geo = (mxGeometry) geo.clone();
geo.translate(dx, dy);
if (!geo.isRelative() && model.isVertex(cell)
&& !isAllowNegativeCoordinates())
{
geo.setX(Math.max(0, geo.getX()));
geo.setY(Math.max(0, geo.getY()));
}
if (geo.isRelative() && !model.isEdge(cell))
{
if (geo.getOffset() == null)
{
geo.setOffset(new mxPoint(dx, dy));
}
else
{
mxPoint offset = geo.getOffset();
offset.setX(offset.getX() + dx);
offset.setY(offset.getY() + dy);
}
}
model.setGeometry(cell, geo);
}
}
/**
* Returns the mxRectangle inside which a cell is to be kept.
*/
public mxRectangle getCellContainmentArea(Object cell)
{
if (cell != null && !model.isEdge(cell))
{
Object parent = model.getParent(cell);
if (parent == getDefaultParent() || parent == getCurrentRoot())
{
return getMaximumGraphBounds();
}
else if (parent != null && parent != getDefaultParent())
{
mxGeometry g = model.getGeometry(parent);
if (g != null)
{
double x = 0;
double y = 0;
double w = g.getWidth();
double h = g.getHeight();
if (isSwimlane(parent))
{
mxRectangle size = getStartSize(parent);
x = size.getWidth();
w -= size.getWidth();
y = size.getHeight();
h -= size.getHeight();
}
return new mxRectangle(x, y, w, h);
}
}
}
return null;
}
/**
* @return the maximumGraphBounds
*/
public mxRectangle getMaximumGraphBounds()
{
return maximumGraphBounds;
}
/**
* @param value the maximumGraphBounds to set
*/
public void setMaximumGraphBounds(mxRectangle value)
{
mxRectangle oldValue = maximumGraphBounds;
maximumGraphBounds = value;
changeSupport.firePropertyChange("maximumGraphBounds", oldValue,
maximumGraphBounds);
}
/**
* Keeps the given cell inside the bounds returned by
* getCellContainmentArea for its parent, according to the rules defined by
* getOverlap and isConstrainChild. This modifies the cell's geometry
* in-place and does not clone it.
*
* @param cell Cell which should be constrained.
*/
public void constrainChild(Object cell)
{
if (cell != null)
{
mxGeometry geo = model.getGeometry(cell);
mxRectangle area = (isConstrainChild(cell)) ? getCellContainmentArea(cell)
: getMaximumGraphBounds();
if (geo != null && area != null)
{
// Keeps child within the content area of the parent
if (!geo.isRelative()
&& (geo.getX() < area.getX()
|| geo.getY() < area.getY()
|| area.getWidth() < geo.getX()
+ geo.getWidth() || area.getHeight() < geo
.getY() + geo.getHeight()))
{
double overlap = getOverlap(cell);
if (area.getWidth() > 0)
{
geo.setX(Math.min(geo.getX(),
area.getX() + area.getWidth() - (1 - overlap)
* geo.getWidth()));
}
if (area.getHeight() > 0)
{
geo.setY(Math.min(geo.getY(),
area.getY() + area.getHeight() - (1 - overlap)
* geo.getHeight()));
}
geo.setX(Math.max(geo.getX(), area.getX() - geo.getWidth()
* overlap));
geo.setY(Math.max(geo.getY(), area.getY() - geo.getHeight()
* overlap));
}
}
}
}
/**
* Resets the control points of the edges that are connected to the given
* cells if not both ends of the edge are in the given cells array.
*
* @param cells Array of mxCells for which the connected edges should be
* reset.
*/
public void resetEdges(Object[] cells)
{
if (cells != null)
{
// Prepares a hashtable for faster cell lookups
HashSet<Object> set = new HashSet<Object>(Arrays.asList(cells));
model.beginUpdate();
try
{
for (int i = 0; i < cells.length; i++)
{
Object[] edges = mxGraphModel.getEdges(model, cells[i]);
if (edges != null)
{
for (int j = 0; j < edges.length; j++)
{
Object source = view.getVisibleTerminal(edges[j],
true);
Object target = view.getVisibleTerminal(edges[j],
false);
// Checks if one of the terminals is not in the given array
if (!set.contains(source) || !set.contains(target))
{
resetEdge(edges[j]);
}
}
}
resetEdges(mxGraphModel.getChildren(model, cells[i]));
}
}
finally
{
model.endUpdate();
}
}
}
/**
* Resets the control points of the given edge.
*/
public Object resetEdge(Object edge)
{
mxGeometry geo = model.getGeometry(edge);
if (geo != null)
{
// Resets the control points
List<mxPoint> points = geo.getPoints();
if (points != null && !points.isEmpty())
{
geo = (mxGeometry) geo.clone();
geo.setPoints(null);
model.setGeometry(edge, geo);
}
}
return edge;
}
//
// Cell connecting and connection constraints
//
/**
* Returns an array of all constraints for the given terminal.
*
* @param terminal Cell state that represents the terminal.
* @param source Specifies if the terminal is the source or target.
*/
public mxConnectionConstraint[] getAllConnectionConstraints(
mxCellState terminal, boolean source)
{
return null;
}
/**
* Returns an connection constraint that describes the given connection
* point. This result can then be passed to getConnectionPoint.
*
* @param edge Cell state that represents the edge.
* @param terminal Cell state that represents the terminal.
* @param source Boolean indicating if the terminal is the source or target.
*/
public mxConnectionConstraint getConnectionConstraint(mxCellState edge,
mxCellState terminal, boolean source)
{
mxPoint point = null;
Object x = edge.getStyle()
.get((source) ? mxConstants.STYLE_EXIT_X
: mxConstants.STYLE_ENTRY_X);
if (x != null)
{
Object y = edge.getStyle().get(
(source) ? mxConstants.STYLE_EXIT_Y
: mxConstants.STYLE_ENTRY_Y);
if (y != null)
{
point = new mxPoint(Double.parseDouble(x.toString()),
Double.parseDouble(y.toString()));
}
}
boolean perimeter = false;
if (point != null)
{
perimeter = mxUtils.isTrue(edge.style,
(source) ? mxConstants.STYLE_EXIT_PERIMETER
: mxConstants.STYLE_ENTRY_PERIMETER, true);
}
return new mxConnectionConstraint(point, perimeter);
}
/**
* Sets the connection constraint that describes the given connection point.
* If no constraint is given then nothing is changed. To remove an existing
* constraint from the given edge, use an empty constraint instead.
*
* @param edge Cell that represents the edge.
* @param terminal Cell that represents the terminal.
* @param source Boolean indicating if the terminal is the source or target.
* @param constraint Optional connection constraint to be used for this connection.
*/
public void setConnectionConstraint(Object edge, Object terminal,
boolean source, mxConnectionConstraint constraint)
{
if (constraint != null)
{
model.beginUpdate();
try
{
Object[] cells = new Object[] { edge };
// FIXME, constraint can't be null, we've checked that above
if (constraint == null || constraint.point == null)
{
setCellStyles((source) ? mxConstants.STYLE_EXIT_X
: mxConstants.STYLE_ENTRY_X, null, cells);
setCellStyles((source) ? mxConstants.STYLE_EXIT_Y
: mxConstants.STYLE_ENTRY_Y, null, cells);
setCellStyles((source) ? mxConstants.STYLE_EXIT_PERIMETER
: mxConstants.STYLE_ENTRY_PERIMETER, null, cells);
}
else if (constraint.point != null)
{
setCellStyles((source) ? mxConstants.STYLE_EXIT_X
: mxConstants.STYLE_ENTRY_X,
String.valueOf(constraint.point.getX()), cells);
setCellStyles((source) ? mxConstants.STYLE_EXIT_Y
: mxConstants.STYLE_ENTRY_Y,
String.valueOf(constraint.point.getY()), cells);
// Only writes 0 since 1 is default
if (!constraint.perimeter)
{
setCellStyles(
(source) ? mxConstants.STYLE_EXIT_PERIMETER
: mxConstants.STYLE_ENTRY_PERIMETER,
"0", cells);
}
else
{
setCellStyles(
(source) ? mxConstants.STYLE_EXIT_PERIMETER
: mxConstants.STYLE_ENTRY_PERIMETER,
null, cells);
}
}
}
finally
{
model.endUpdate();
}
}
}
/**
* Sets the connection constraint that describes the given connection point.
* If no constraint is given then nothing is changed. To remove an existing
* constraint from the given edge, use an empty constraint instead.
*
* @param vertex Cell state that represents the vertex.
* @param constraint Connection constraint that represents the connection point
* constraint as returned by getConnectionConstraint.
*/
public mxPoint getConnectionPoint(mxCellState vertex,
mxConnectionConstraint constraint)
{
mxPoint point = null;
if (vertex != null && constraint.point != null)
{
point = new mxPoint(vertex.getX() + constraint.getPoint().getX()
* vertex.getWidth(), vertex.getY()
+ constraint.getPoint().getY() * vertex.getHeight());
}
if (point != null && constraint.perimeter)
{
point = view.getPerimeterPoint(vertex, point, false);
}
return point;
}
/**
* Connects the specified end of the given edge to the given terminal
* using cellConnected and fires mxEvent.CONNECT_CELL while the transaction
* is in progress.
*/
public Object connectCell(Object edge, Object terminal, boolean source)
{
return connectCell(edge, terminal, source, null);
}
/**
* Connects the specified end of the given edge to the given terminal
* using cellConnected and fires mxEvent.CONNECT_CELL while the transaction
* is in progress.
*
* @param edge Edge whose terminal should be updated.
* @param terminal New terminal to be used.
* @param source Specifies if the new terminal is the source or target.
* @param constraint Optional constraint to be used for this connection.
* @return Returns the update edge.
*/
public Object connectCell(Object edge, Object terminal, boolean source,
mxConnectionConstraint constraint)
{
model.beginUpdate();
try
{
Object previous = model.getTerminal(edge, source);
cellConnected(edge, terminal, source, constraint);
fireEvent(new mxEventObject(mxEvent.CONNECT_CELL, "edge", edge,
"terminal", terminal, "source", source, "previous",
previous));
}
finally
{
model.endUpdate();
}
return edge;
}
/**
* Sets the new terminal for the given edge and resets the edge points if
* isResetEdgesOnConnect returns true. This method fires
* <mxEvent.CELL_CONNECTED> while the transaction is in progress.
*
* @param edge Edge whose terminal should be updated.
* @param terminal New terminal to be used.
* @param source Specifies if the new terminal is the source or target.
* @param constraint Constraint to be used for this connection.
*/
public void cellConnected(Object edge, Object terminal, boolean source,
mxConnectionConstraint constraint)
{
if (edge != null)
{
model.beginUpdate();
try
{
Object previous = model.getTerminal(edge, source);
// Updates the constraint
setConnectionConstraint(edge, terminal, source, constraint);
// Checks if the new terminal is a port
String id = null;
if (isPort(terminal) && terminal instanceof mxICell)
{
id = ((mxICell) terminal).getId();
terminal = getTerminalForPort(terminal, source);
}
// Sets or resets all previous information for connecting to a child port
String key = (source) ? mxConstants.STYLE_SOURCE_PORT
: mxConstants.STYLE_TARGET_PORT;
setCellStyles(key, id, new Object[] { edge });
model.setTerminal(edge, terminal, source);
if (isResetEdgesOnConnect())
{
resetEdge(edge);
}
fireEvent(new mxEventObject(mxEvent.CELL_CONNECTED, "edge",
edge, "terminal", terminal, "source", source,
"previous", previous));
}
finally
{
model.endUpdate();
}
}
}
/**
* Disconnects the given edges from the terminals which are not in the
* given array.
*
* @param cells Array of <mxCells> to be disconnected.
*/
public void disconnectGraph(Object[] cells)
{
if (cells != null)
{
model.beginUpdate();
try
{
double scale = view.getScale();
mxPoint tr = view.getTranslate();
// Prepares a hashtable for faster cell lookups
Set<Object> hash = new HashSet<Object>();
for (int i = 0; i < cells.length; i++)
{
hash.add(cells[i]);
}
for (int i = 0; i < cells.length; i++)
{
if (model.isEdge(cells[i]))
{
mxGeometry geo = model.getGeometry(cells[i]);
if (geo != null)
{
mxCellState state = view.getState(cells[i]);
mxCellState pstate = view.getState(model
.getParent(cells[i]));
if (state != null && pstate != null)
{
geo = (mxGeometry) geo.clone();
double dx = -pstate.getOrigin().getX();
double dy = -pstate.getOrigin().getY();
Object src = model.getTerminal(cells[i], true);
if (src != null
&& isCellDisconnectable(cells[i], src,
true))
{
while (src != null && !hash.contains(src))
{
src = model.getParent(src);
}
if (src == null)
{
mxPoint pt = state.getAbsolutePoint(0);
geo.setTerminalPoint(
new mxPoint(pt.getX() / scale
- tr.getX() + dx, pt
.getY()
/ scale
- tr.getY() + dy), true);
model.setTerminal(cells[i], null, true);
}
}
Object trg = model.getTerminal(cells[i], false);
if (trg != null
&& isCellDisconnectable(cells[i], trg,
false))
{
while (trg != null && !hash.contains(trg))
{
trg = model.getParent(trg);
}
if (trg == null)
{
int n = state.getAbsolutePointCount() - 1;
mxPoint pt = state.getAbsolutePoint(n);
geo.setTerminalPoint(
new mxPoint(pt.getX() / scale
- tr.getX() + dx, pt
.getY()
/ scale
- tr.getY() + dy),
false);
model.setTerminal(cells[i], null, false);
}
}
}
model.setGeometry(cells[i], geo);
}
}
}
}
finally
{
model.endUpdate();
}
}
}
//
// Drilldown
//
/**
* Returns the current root of the displayed cell hierarchy. This is a
* shortcut to <mxGraphView.currentRoot> in <view>.
*
* @return Returns the current root in the view.
*/
public Object getCurrentRoot()
{
return view.getCurrentRoot();
}
/**
* Returns the translation to be used if the given cell is the root cell as
* an <mxPoint>. This implementation returns null.
*
* @param cell Cell that represents the root of the view.
* @return Returns the translation of the graph for the given root cell.
*/
public mxPoint getTranslateForRoot(Object cell)
{
return null;
}
/**
* Returns true if the given cell is a "port", that is, when connecting to
* it, the cell returned by getTerminalForPort should be used as the
* terminal and the port should be referenced by the ID in either the
* mxConstants.STYLE_SOURCE_PORT or the or the
* mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable.
* This implementation always returns false.
*
* A typical implementation of this method looks as follows:
*
* <code>
* public boolean isPort(Object cell)
* {
* mxGeometry geo = getCellGeometry(cell);
*
* return (geo != null) ? geo.isRelative() : false;
* }
* </code>
*
* @param cell Cell that represents the port.
* @return Returns true if the cell is a port.
*/
public boolean isPort(Object cell)
{
return false;
}
/**
* Returns the terminal to be used for a given port. This implementation
* always returns the parent cell.
*
* @param cell Cell that represents the port.
* @param source If the cell is the source or target port.
* @return Returns the terminal to be used for the given port.
*/
public Object getTerminalForPort(Object cell, boolean source)
{
return getModel().getParent(cell);
}
/**
* Returns the offset to be used for the cells inside the given cell. The
* root and layer cells may be identified using mxGraphModel.isRoot and
* mxGraphModel.isLayer. This implementation returns null.
*
* @param cell Cell whose offset should be returned.
* @return Returns the child offset for the given cell.
*/
public mxPoint getChildOffsetForCell(Object cell)
{
return null;
}
/**
*
*/
public void enterGroup()
{
enterGroup(null);
}
/**
* Uses the given cell as the root of the displayed cell hierarchy. If no
* cell is specified then the selection cell is used. The cell is only used
* if <isValidRoot> returns true.
*
* @param cell
*/
public void enterGroup(Object cell)
{
if (cell == null)
{
cell = getSelectionCell();
}
if (cell != null && isValidRoot(cell))
{
view.setCurrentRoot(cell);
clearSelection();
}
}
/**
* Changes the current root to the next valid root in the displayed cell
* hierarchy.
*/
public void exitGroup()
{
Object root = model.getRoot();
Object current = getCurrentRoot();
if (current != null)
{
Object next = model.getParent(current);
// Finds the next valid root in the hierarchy
while (next != root && !isValidRoot(next)
&& model.getParent(next) != root)
{
next = model.getParent(next);
}
// Clears the current root if the new root is
// the model's root or one of the layers.
if (next == root || model.getParent(next) == root)
{
view.setCurrentRoot(null);
}
else
{
view.setCurrentRoot(next);
}
mxCellState state = view.getState(current);
// Selects the previous root in the graph
if (state != null)
{
setSelectionCell(current);
}
}
}
/**
* Uses the root of the model as the root of the displayed cell hierarchy
* and selects the previous root.
*/
public void home()
{
Object current = getCurrentRoot();
if (current != null)
{
view.setCurrentRoot(null);
mxCellState state = view.getState(current);
if (state != null)
{
setSelectionCell(current);
}
}
}
/**
* Returns true if the given cell is a valid root for the cell display
* hierarchy. This implementation returns true for all non-null values.
*
* @param cell <mxCell> which should be checked as a possible root.
* @return Returns true if the given cell is a valid root.
*/
public boolean isValidRoot(Object cell)
{
return (cell != null);
}
//
// Graph display
//
/**
* Returns the bounds of the visible graph.
*/
public mxRectangle getGraphBounds()
{
return view.getGraphBounds();
}
/**
* Returns the bounds of the given cell.
*/
public mxRectangle getCellBounds(Object cell)
{
return getCellBounds(cell, false);
}
/**
* Returns the bounds of the given cell including all connected edges
* if includeEdge is true.
*/
public mxRectangle getCellBounds(Object cell, boolean includeEdges)
{
return getCellBounds(cell, includeEdges, false);
}
/**
* Returns the bounds of the given cell including all connected edges
* if includeEdge is true.
*/
public mxRectangle getCellBounds(Object cell, boolean includeEdges,
boolean includeDescendants)
{
return getCellBounds(cell, includeEdges, includeDescendants, false);
}
/**
* Returns the bounding box for the geometries of the vertices in the
* given array of cells.
*/
public mxRectangle getBoundingBoxFromGeometry(Object[] cells)
{
mxRectangle result = null;
if (cells != null)
{
for (int i = 0; i < cells.length; i++)
{
if (getModel().isVertex(cells[i]))
{
mxGeometry geo = getCellGeometry(cells[i]);
if (result == null)
{
result = new mxRectangle(geo);
}
else
{
result.add(geo);
}
}
}
}
return result;
}
/**
* Returns the bounds of the given cell.
*/
public mxRectangle getBoundingBox(Object cell)
{
return getBoundingBox(cell, false);
}
/**
* Returns the bounding box of the given cell including all connected edges
* if includeEdge is true.
*/
public mxRectangle getBoundingBox(Object cell, boolean includeEdges)
{
return getBoundingBox(cell, includeEdges, false);
}
/**
* Returns the bounding box of the given cell including all connected edges
* if includeEdge is true.
*/
public mxRectangle getBoundingBox(Object cell, boolean includeEdges,
boolean includeDescendants)
{
return getCellBounds(cell, includeEdges, includeDescendants, true);
}
/**
* Returns the bounding box of the given cells and their descendants.
*/
public mxRectangle getPaintBounds(Object[] cells)
{
return getBoundsForCells(cells, false, true, true);
}
/**
* Returns the bounds for the given cells.
*/
public mxRectangle getBoundsForCells(Object[] cells, boolean includeEdges,
boolean includeDescendants, boolean boundingBox)
{
mxRectangle result = null;
if (cells != null && cells.length > 0)
{
for (int i = 0; i < cells.length; i++)
{
mxRectangle tmp = getCellBounds(cells[i], includeEdges,
includeDescendants, boundingBox);
if (tmp != null)
{
if (result == null)
{
result = new mxRectangle(tmp);
}
else
{
result.add(tmp);
}
}
}
}
return result;
}
/**
* Returns the bounds of the given cell including all connected edges
* if includeEdge is true.
*/
public mxRectangle getCellBounds(Object cell, boolean includeEdges,
boolean includeDescendants, boolean boundingBox)
{
mxRectangle result = null;
Object[] cells;
// Recursively includes connected edges
if (includeEdges)
{
Set<Object> allCells = new HashSet<Object>();
allCells.add(cell);
Set<Object> edges = new HashSet<Object>(
Arrays.asList(getEdges(cell)));
while (!edges.isEmpty() && !allCells.containsAll(edges))
{
allCells.addAll(edges);
Set<Object> tmp = new HashSet<Object>();
Iterator<Object> it = edges.iterator();
while (it.hasNext())
{
Object edge = it.next();
tmp.addAll(Arrays.asList(getEdges(edge)));
}
edges = tmp;
}
cells = allCells.toArray();
}
else
{
cells = new Object[] { cell };
}
if (boundingBox)
{
result = view.getBoundingBox(cells);
}
else
{
result = view.getBounds(cells);
}
// Recursively includes the bounds of the children
if (includeDescendants)
{
int childCount = model.getChildCount(cell);
for (int i = 0; i < childCount; i++)
{
mxRectangle tmp = getCellBounds(model.getChildAt(cell, i),
includeEdges, true, boundingBox);
if (result != null)
{
result.add(tmp);
}
else
{
result = tmp;
}
}
}
return result;
}
/**
* Clears all cell states or the states for the hierarchy starting at the
* given cell and validates the graph.
*/
public void refresh()
{
view.reload();
repaint();
}
/**
* Fires a repaint event.
*/
public void repaint()
{
repaint(null);
}
/**
* Fires a repaint event. The optional region is the rectangle that needs
* to be repainted.
*/
public void repaint(mxRectangle region)
{
fireEvent(new mxEventObject(mxEvent.REPAINT, "region", region));
}
/**
* Snaps the given numeric value to the grid if <gridEnabled> is true.
*
* @param value Numeric value to be snapped to the grid.
* @return Returns the value aligned to the grid.
*/
public double snap(double value)
{
if (gridEnabled)
{
value = Math.round(value / gridSize) * gridSize;
}
return value;
}
/**
* Returns the geometry for the given cell.
*
* @param cell Cell whose geometry should be returned.
* @return Returns the geometry of the cell.
*/
public mxGeometry getCellGeometry(Object cell)
{
return model.getGeometry(cell);
}
/**
* Returns true if the given cell is visible in this graph. This
* implementation uses <mxGraphModel.isVisible>. Subclassers can override
* this to implement specific visibility for cells in only one graph, that
* is, without affecting the visible state of the cell.
*
* When using dynamic filter expressions for cell visibility, then the
* graph should be revalidated after the filter expression has changed.
*
* @param cell Cell whose visible state should be returned.
* @return Returns the visible state of the cell.
*/
public boolean isCellVisible(Object cell)
{
return model.isVisible(cell);
}
/**
* Returns true if the given cell is collapsed in this graph. This
* implementation uses <mxGraphModel.isCollapsed>. Subclassers can override
* this to implement specific collapsed states for cells in only one graph,
* that is, without affecting the collapsed state of the cell.
*
* When using dynamic filter expressions for the collapsed state, then the
* graph should be revalidated after the filter expression has changed.
*
* @param cell Cell whose collapsed state should be returned.
* @return Returns the collapsed state of the cell.
*/
public boolean isCellCollapsed(Object cell)
{
return model.isCollapsed(cell);
}
/**
* Returns true if the given cell is connectable in this graph. This
* implementation uses <mxGraphModel.isConnectable>. Subclassers can override
* this to implement specific connectable states for cells in only one graph,
* that is, without affecting the connectable state of the cell in the model.
*
* @param cell Cell whose connectable state should be returned.
* @return Returns the connectable state of the cell.
*/
public boolean isCellConnectable(Object cell)
{
return model.isConnectable(cell);
}
/**
* Returns true if perimeter points should be computed such that the
* resulting edge has only horizontal or vertical segments.
*
* @param edge Cell state that represents the edge.
*/
public boolean isOrthogonal(mxCellState edge)
{
if (edge.getStyle().containsKey(mxConstants.STYLE_ORTHOGONAL))
{
return mxUtils
.isTrue(edge.getStyle(), mxConstants.STYLE_ORTHOGONAL);
}
mxEdgeStyle.mxEdgeStyleFunction tmp = view.getEdgeStyle(edge, null,
null, null);
return tmp == mxEdgeStyle.ElbowConnector
|| tmp == mxEdgeStyle.SideToSide
|| tmp == mxEdgeStyle.TopToBottom
|| tmp == mxEdgeStyle.EntityRelation;
}
/**
* Returns true if the given cell state is a loop.
*
* @param state <mxCellState> that represents a potential loop.
* @return Returns true if the given cell is a loop.
*/
public boolean isLoop(mxCellState state)
{
Object src = view.getVisibleTerminal(state.getCell(), true);
Object trg = view.getVisibleTerminal(state.getCell(), false);
return (src != null && src == trg);
}
//
// Cell validation
//
/**
*
*/
public void setMultiplicities(mxMultiplicity[] value)
{
mxMultiplicity[] oldValue = multiplicities;
multiplicities = value;
changeSupport.firePropertyChange("multiplicities", oldValue,
multiplicities);
}
/**
*
*/
public mxMultiplicity[] getMultiplicities()
{
return multiplicities;
}
/**
* Checks if the return value of getEdgeValidationError for the given
* arguments is null.
*
* @param edge Cell that represents the edge to validate.
* @param source Cell that represents the source terminal.
* @param target Cell that represents the target terminal.
*/
public boolean isEdgeValid(Object edge, Object source, Object target)
{
return getEdgeValidationError(edge, source, target) == null;
}
/**
* Returns the validation error message to be displayed when inserting or
* changing an edges' connectivity. A return value of null means the edge
* is valid, a return value of '' means it's not valid, but do not display
* an error message. Any other (non-empty) string returned from this method
* is displayed as an error message when trying to connect an edge to a
* source and target. This implementation uses the multiplicities, as
* well as multigraph and allowDanglingEdges to generate validation
* errors.
*
* @param edge Cell that represents the edge to validate.
* @param source Cell that represents the source terminal.
* @param target Cell that represents the target terminal.
*/
public String getEdgeValidationError(Object edge, Object source,
Object target)
{
if (edge != null && model.getTerminal(edge, true) == null
&& model.getTerminal(edge, false) == null)
{
return null;
}
// Checks if we're dealing with a loop
if (!isAllowLoops() && source == target && source != null)
{
return "";
}
// Checks if the connection is generally allowed
if (!isValidConnection(source, target))
{
return "";
}
if (source != null && target != null)
{
StringBuffer error = new StringBuffer();
// Checks if the cells are already connected
// and adds an error message if required
if (!multigraph)
{
Object[] tmp = mxGraphModel.getEdgesBetween(model, source,
target, true);
// Checks if the source and target are not connected by another edge
if (tmp.length > 1 || (tmp.length == 1 && tmp[0] != edge))
{
error.append(mxResources.get("alreadyConnected",
"Already Connected") + "\n");
}
}
// Gets the number of outgoing edges from the source
// and the number of incoming edges from the target
// without counting the edge being currently changed.
int sourceOut = mxGraphModel.getDirectedEdgeCount(model, source,
true, edge);
int targetIn = mxGraphModel.getDirectedEdgeCount(model, target,
false, edge);
// Checks the change against each multiplicity rule
if (multiplicities != null)
{
for (int i = 0; i < multiplicities.length; i++)
{
String err = multiplicities[i].check(this, edge, source,
target, sourceOut, targetIn);
if (err != null)
{
error.append(err);
}
}
}
// Validates the source and target terminals independently
String err = validateEdge(edge, source, target);
if (err != null)
{
error.append(err);
}
return (error.length() > 0) ? error.toString() : null;
}
return (allowDanglingEdges) ? null : "";
}
/**
* Hook method for subclassers to return an error message for the given
* edge and terminals. This implementation returns null.
*
* @param edge Cell that represents the edge to validate.
* @param source Cell that represents the source terminal.
* @param target Cell that represents the target terminal.
*/
public String validateEdge(Object edge, Object source, Object target)
{
return null;
}
/**
* Checks all multiplicities that cannot be enforced while the graph is
* being modified, namely, all multiplicities that require a minimum of
* 1 edge.
*
* @param cell Cell for which the multiplicities should be checked.
*/
public String getCellValidationError(Object cell)
{
int outCount = mxGraphModel.getDirectedEdgeCount(model, cell, true);
int inCount = mxGraphModel.getDirectedEdgeCount(model, cell, false);
StringBuffer error = new StringBuffer();
Object value = model.getValue(cell);
for (int i = 0; i < multiplicities.length; i++)
{
mxMultiplicity rule = multiplicities[i];
int max = rule.getMaxValue();
if (rule.source
&& mxUtils.isNode(value, rule.type, rule.attr, rule.value)
&& ((max == 0 && outCount > 0)
|| (rule.min == 1 && outCount == 0) || (max == 1 && outCount > 1)))
{
error.append(rule.countError + '\n');
}
else if (!rule.source
&& mxUtils.isNode(value, rule.type, rule.attr, rule.value)
&& ((max == 0 && inCount > 0)
|| (rule.min == 1 && inCount == 0) || (max == 1 && inCount > 1)))
{
error.append(rule.countError + '\n');
}
}
return (error.length() > 0) ? error.toString() : null;
}
/**
* Hook method for subclassers to return an error message for the given
* cell and validation context. This implementation returns null.
*
* @param cell Cell that represents the cell to validate.
* @param context Hashtable that represents the global validation state.
*/
public String validateCell(Object cell, Hashtable<Object, Object> context)
{
return null;
}
//
// Graph appearance
//
/**
* @return the labelsVisible
*/
public boolean isLabelsVisible()
{
return labelsVisible;
}
/**
* @param value the labelsVisible to set
*/
public void setLabelsVisible(boolean value)
{
boolean oldValue = labelsVisible;
labelsVisible = value;
changeSupport.firePropertyChange("labelsVisible", oldValue,
labelsVisible);
}
/**
* @param value the htmlLabels to set
*/
public void setHtmlLabels(boolean value)
{
boolean oldValue = htmlLabels;
htmlLabels = value;
changeSupport.firePropertyChange("htmlLabels", oldValue, htmlLabels);
}
/**
*
*/
public boolean isHtmlLabels()
{
return htmlLabels;
}
/**
* Returns the textual representation for the given cell.
*
* @param cell Cell to be converted to a string.
* @return Returns the textual representation of the cell.
*/
public String convertValueToString(Object cell)
{
Object result = model.getValue(cell);
return (result != null) ? result.toString() : "";
}
/**
* Returns a string or DOM node that represents the label for the given
* cell. This implementation uses <convertValueToString> if <labelsVisible>
* is true. Otherwise it returns an empty string.
*
* @param cell <mxCell> whose label should be returned.
* @return Returns the label for the given cell.
*/
public String getLabel(Object cell)
{
String result = "";
if (cell != null)
{
mxCellState state = view.getState(cell);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(cell);
if (labelsVisible
&& !mxUtils.isTrue(style, mxConstants.STYLE_NOLABEL, false))
{
result = convertValueToString(cell);
}
}
return result;
}
/**
* Sets the new label for a cell. If autoSize is true then
* <cellSizeUpdated> will be called.
*
* @param cell Cell whose label should be changed.
* @param value New label to be assigned.
* @param autoSize Specifies if cellSizeUpdated should be called.
*/
public void cellLabelChanged(Object cell, Object value, boolean autoSize)
{
model.beginUpdate();
try
{
getModel().setValue(cell, value);
if (autoSize)
{
cellSizeUpdated(cell, false);
}
}
finally
{
model.endUpdate();
}
}
/**
* Returns true if the label must be rendered as HTML markup. The default
* implementation returns <htmlLabels>.
*
* @param cell <mxCell> whose label should be displayed as HTML markup.
* @return Returns true if the given cell label is HTML markup.
*/
public boolean isHtmlLabel(Object cell)
{
return isHtmlLabels();
}
/**
* Returns the tooltip to be used for the given cell.
*/
public String getToolTipForCell(Object cell)
{
return convertValueToString(cell);
}
/**
* Returns the start size of the given swimlane, that is, the width or
* height of the part that contains the title, depending on the
* horizontal style. The return value is an <mxRectangle> with either
* width or height set as appropriate.
*
* @param swimlane <mxCell> whose start size should be returned.
* @return Returns the startsize for the given swimlane.
*/
public mxRectangle getStartSize(Object swimlane)
{
mxRectangle result = new mxRectangle();
mxCellState state = view.getState(swimlane);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(swimlane);
if (style != null)
{
double size = mxUtils.getDouble(style, mxConstants.STYLE_STARTSIZE,
mxConstants.DEFAULT_STARTSIZE);
if (mxUtils.isTrue(style, mxConstants.STYLE_HORIZONTAL, true))
{
result.setHeight(size);
}
else
{
result.setWidth(size);
}
}
return result;
}
/**
* Returns the image URL for the given cell state. This implementation
* returns the value stored under <mxConstants.STYLE_IMAGE> in the cell
* style.
*
* @param state
* @return Returns the image associated with the given cell state.
*/
public String getImage(mxCellState state)
{
return (state != null && state.getStyle() != null) ? mxUtils.getString(
state.getStyle(), mxConstants.STYLE_IMAGE) : null;
}
/**
* Returns the value of <border>.
*
* @return Returns the border.
*/
public int getBorder()
{
return border;
}
/**
* Sets the value of <border>.
*
* @param value Positive integer that represents the border to be used.
*/
public void setBorder(int value)
{
border = value;
}
/**
* Returns the default edge style used for loops.
*
* @return Returns the default loop style.
*/
public mxEdgeStyle.mxEdgeStyleFunction getDefaultLoopStyle()
{
return defaultLoopStyle;
}
/**
* Sets the default style used for loops.
*
* @param value Default style to be used for loops.
*/
public void setDefaultLoopStyle(mxEdgeStyle.mxEdgeStyleFunction value)
{
mxEdgeStyle.mxEdgeStyleFunction oldValue = defaultLoopStyle;
defaultLoopStyle = value;
changeSupport.firePropertyChange("defaultLoopStyle", oldValue,
defaultLoopStyle);
}
/**
* Returns true if the given cell is a swimlane. This implementation always
* returns false.
*
* @param cell Cell that should be checked.
* @return Returns true if the cell is a swimlane.
*/
public boolean isSwimlane(Object cell)
{
if (cell != null)
{
if (model.getParent(cell) != model.getRoot())
{
mxCellState state = view.getState(cell);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(cell);
if (style != null && !model.isEdge(cell))
{
return mxUtils
.getString(style, mxConstants.STYLE_SHAPE, "")
.equals(mxConstants.SHAPE_SWIMLANE);
}
}
}
return false;
}
//
// Cells and labels control options
//
/**
* Returns true if the given cell may not be moved, sized, bended,
* disconnected, edited or selected. This implementation returns true for
* all vertices with a relative geometry if cellsLocked is false.
*
* @param cell Cell whose locked state should be returned.
* @return Returns true if the given cell is locked.
*/
public boolean isCellLocked(Object cell)
{
mxGeometry geometry = model.getGeometry(cell);
return isCellsLocked()
|| (geometry != null && model.isVertex(cell) && geometry
.isRelative());
}
/**
* Returns cellsLocked, the default return value for isCellLocked.
*/
public boolean isCellsLocked()
{
return cellsLocked;
}
/**
* Sets cellsLocked, the default return value for isCellLocked and fires a
* property change event for cellsLocked.
*/
public void setCellsLocked(boolean value)
{
boolean oldValue = cellsLocked;
cellsLocked = value;
changeSupport.firePropertyChange("cellsLocked", oldValue, cellsLocked);
}
/**
* Returns true if the given cell is movable. This implementation returns editable.
*
* @param cell Cell whose editable state should be returned.
* @return Returns true if the cell is editable.
*/
public boolean isCellEditable(Object cell)
{
return isCellsEditable() && !isCellLocked(cell);
}
/**
* Returns true if editing is allowed in this graph.
*
* @return Returns true if the graph is editable.
*/
public boolean isCellsEditable()
{
return cellsEditable;
}
/**
* Sets if the graph is editable.
*/
public void setCellsEditable(boolean value)
{
boolean oldValue = cellsEditable;
cellsEditable = value;
changeSupport.firePropertyChange("cellsEditable", oldValue,
cellsEditable);
}
/**
* Returns true if the given cell is resizable. This implementation returns
* cellsSizable for all cells.
*
* @param cell Cell whose resizable state should be returned.
* @return Returns true if the cell is sizable.
*/
public boolean isCellResizable(Object cell)
{
return isCellsResizable() && !isCellLocked(cell);
}
/**
* Returns true if the given cell is resizable. This implementation return sizable.
*/
public boolean isCellsResizable()
{
return cellsResizable;
}
/**
* Sets if the graph is resizable.
*/
public void setCellsResizable(boolean value)
{
boolean oldValue = cellsResizable;
cellsResizable = value;
changeSupport.firePropertyChange("cellsResizable", oldValue,
cellsResizable);
}
/**
* Returns the cells which are movable in the given array of cells.
*/
public Object[] getMovableCells(Object[] cells)
{
return mxGraphModel.filterCells(cells, new Filter()
{
public boolean filter(Object cell)
{
return isCellMovable(cell);
}
});
}
/**
* Returns true if the given cell is movable. This implementation
* returns movable.
*
* @param cell Cell whose movable state should be returned.
* @return Returns true if the cell is movable.
*/
public boolean isCellMovable(Object cell)
{
return isCellsMovable() && !isCellLocked(cell);
}
/**
* Returns cellsMovable.
*/
public boolean isCellsMovable()
{
return cellsMovable;
}
/**
* Sets cellsMovable.
*/
public void setCellsMovable(boolean value)
{
boolean oldValue = cellsMovable;
cellsMovable = value;
changeSupport
.firePropertyChange("cellsMovable", oldValue, cellsMovable);
}
/**
* Returns true if the given cell is bendable. This implementation returns
* bendable. This is used in mxElbowEdgeHandler to determine if the middle
* handle should be shown.
*
* @param cell Cell whose bendable state should be returned.
* @return Returns true if the cell is bendable.
*/
public boolean isCellBendable(Object cell)
{
return isCellsBendable() && !isCellLocked(cell);
}
/**
* Returns cellsBendable.
*/
public boolean isCellsBendable()
{
return cellsBendable;
}
/**
* Sets cellsBendable.
*/
public void setCellsBendable(boolean value)
{
boolean oldValue = cellsBendable;
cellsBendable = value;
changeSupport.firePropertyChange("cellsBendable", oldValue,
cellsBendable);
}
/**
* Returns true if the given cell is selectable. This implementation returns
* <selectable>.
*
* @param cell <mxCell> whose selectable state should be returned.
* @return Returns true if the given cell is selectable.
*/
public boolean isCellSelectable(Object cell)
{
return isCellsSelectable();
}
/**
* Returns cellsSelectable.
*/
public boolean isCellsSelectable()
{
return cellsSelectable;
}
/**
* Sets cellsSelectable.
*/
public void setCellsSelectable(boolean value)
{
boolean oldValue = cellsSelectable;
cellsSelectable = value;
changeSupport.firePropertyChange("cellsSelectable", oldValue,
cellsSelectable);
}
/**
* Returns the cells which are movable in the given array of cells.
*/
public Object[] getDeletableCells(Object[] cells)
{
return mxGraphModel.filterCells(cells, new Filter()
{
public boolean filter(Object cell)
{
return isCellDeletable(cell);
}
});
}
/**
* Returns true if the given cell is movable. This implementation always
* returns true.
*
* @param cell Cell whose movable state should be returned.
* @return Returns true if the cell is movable.
*/
public boolean isCellDeletable(Object cell)
{
return isCellsDeletable();
}
/**
* Returns cellsDeletable.
*/
public boolean isCellsDeletable()
{
return cellsDeletable;
}
/**
* Sets cellsDeletable.
*/
public void setCellsDeletable(boolean value)
{
boolean oldValue = cellsDeletable;
cellsDeletable = value;
changeSupport.firePropertyChange("cellsDeletable", oldValue,
cellsDeletable);
}
/**
* Returns the cells which are movable in the given array of cells.
*/
public Object[] getCloneableCells(Object[] cells)
{
return mxGraphModel.filterCells(cells, new Filter()
{
public boolean filter(Object cell)
{
return isCellCloneable(cell);
}
});
}
/**
* Returns the constant true. This does not use the cloneable field to
* return a value for a given cell, it is simply a hook for subclassers
* to disallow cloning of individual cells.
*/
public boolean isCellCloneable(Object cell)
{
return isCellsCloneable();
}
/**
* Returns cellsCloneable.
*/
public boolean isCellsCloneable()
{
return cellsCloneable;
}
/**
* Specifies if the graph should allow cloning of cells by holding down the
* control key while cells are being moved. This implementation updates
* cellsCloneable.
*
* @param value Boolean indicating if the graph should be cloneable.
*/
public void setCellsCloneable(boolean value)
{
boolean oldValue = cellsCloneable;
cellsCloneable = value;
changeSupport.firePropertyChange("cellsCloneable", oldValue,
cellsCloneable);
}
/**
* Returns true if the given cell is disconnectable from the source or
* target terminal. This returns <disconnectable> for all given cells if
* <isLocked> does not return true for the given cell.
*
* @param cell <mxCell> whose disconnectable state should be returned.
* @param terminal <mxCell> that represents the source or target terminal.
* @param source Boolean indicating if the source or target terminal is to be
* disconnected.
* @return Returns true if the given edge can be disconnected from the given
* terminal.
*/
public boolean isCellDisconnectable(Object cell, Object terminal,
boolean source)
{
return isCellsDisconnectable() && !isCellLocked(cell);
}
/**
* Returns cellsDisconnectable.
*/
public boolean isCellsDisconnectable()
{
return cellsDisconnectable;
}
/**
* Sets cellsDisconnectable.
*
* @param value Boolean indicating if the graph should allow disconnecting of
* edges.
*/
public void setCellsDisconnectable(boolean value)
{
boolean oldValue = cellsDisconnectable;
cellsDisconnectable = value;
changeSupport.firePropertyChange("cellsDisconnectable", oldValue,
cellsDisconnectable);
}
/**
* Returns true if the overflow portion of labels should be hidden. If this
* returns true then vertex labels will be clipped to the size of the vertices.
* This implementation returns true if <mxConstants.STYLE_OVERFLOW> in the
* style of the given cell is "hidden".
*
* @param cell Cell whose label should be clipped.
* @return Returns true if the cell label should be clipped.
*/
public boolean isLabelClipped(Object cell)
{
if (!isLabelsClipped())
{
mxCellState state = view.getState(cell);
Map<String, Object> style = (state != null) ? state.getStyle()
: getCellStyle(cell);
return (style != null) ? mxUtils.getString(style,
mxConstants.STYLE_OVERFLOW, "").equals("hidden") : false;
}
return isLabelsClipped();
}
/**
* Returns labelsClipped.
*/
public boolean isLabelsClipped()
{
return labelsClipped;
}
/**
* Sets labelsClipped.
*/
public void setLabelsClipped(boolean value)
{
boolean oldValue = labelsClipped;
labelsClipped = value;
changeSupport.firePropertyChange("labelsClipped", oldValue,
labelsClipped);
}
/**
* Returns true if the given edges's label is moveable. This returns
* <movable> for all given cells if <isLocked> does not return true
* for the given cell.
*
* @param cell <mxCell> whose label should be moved.
* @return Returns true if the label of the given cell is movable.
*/
public boolean isLabelMovable(Object cell)
{
return !isCellLocked(cell)
&& ((model.isEdge(cell) && isEdgeLabelsMovable()) || (model
.isVertex(cell) && isVertexLabelsMovable()));
}
/**
* Returns vertexLabelsMovable.
*/
public boolean isVertexLabelsMovable()
{
return vertexLabelsMovable;
}
/**
* Sets vertexLabelsMovable.
*/
public void setVertexLabelsMovable(boolean value)
{
boolean oldValue = vertexLabelsMovable;
vertexLabelsMovable = value;
changeSupport.firePropertyChange("vertexLabelsMovable", oldValue,
vertexLabelsMovable);
}
/**
* Returns edgeLabelsMovable.
*/
public boolean isEdgeLabelsMovable()
{
return edgeLabelsMovable;
}
/**
* Returns edgeLabelsMovable.
*/
public void setEdgeLabelsMovable(boolean value)
{
boolean oldValue = edgeLabelsMovable;
edgeLabelsMovable = value;
changeSupport.firePropertyChange("edgeLabelsMovable", oldValue,
edgeLabelsMovable);
}
//
// Graph control options
//
/**
* Returns true if the graph is <enabled>.
*
* @return Returns true if the graph is enabled.
*/
public boolean isEnabled()
{
return enabled;
}
/**
* Specifies if the graph should allow any interactions. This
* implementation updates <enabled>.
*
* @param value Boolean indicating if the graph should be enabled.
*/
public void setEnabled(boolean value)
{
boolean oldValue = enabled;
enabled = value;
changeSupport.firePropertyChange("enabled", oldValue, enabled);
}
/**
* Returns true if the graph allows drop into other cells.
*/
public boolean isDropEnabled()
{
return dropEnabled;
}
/**
* Sets dropEnabled.
*/
public void setDropEnabled(boolean value)
{
boolean oldValue = dropEnabled;
dropEnabled = value;
changeSupport.firePropertyChange("dropEnabled", oldValue, dropEnabled);
}
/**
* Affects the return values of isValidDropTarget to allow for edges as
* drop targets. The splitEdge method is called in mxGraphHandler if
* mxGraphComponent.isSplitEvent returns true for a given configuration.
*/
public boolean isSplitEnabled()
{
return splitEnabled;
}
/**
* Sets splitEnabled.
*/
public void setSplitEnabled(boolean value)
{
splitEnabled = value;
}
/**
* Returns multigraph.
*/
public boolean isMultigraph()
{
return multigraph;
}
/**
* Sets multigraph.
*/
public void setMultigraph(boolean value)
{
boolean oldValue = multigraph;
multigraph = value;
changeSupport.firePropertyChange("multigraph", oldValue, multigraph);
}
/**
* Returns swimlaneNesting.
*/
public boolean isSwimlaneNesting()
{
return swimlaneNesting;
}
/**
* Sets swimlaneNesting.
*/
public void setSwimlaneNesting(boolean value)
{
boolean oldValue = swimlaneNesting;
swimlaneNesting = value;
changeSupport.firePropertyChange("swimlaneNesting", oldValue,
swimlaneNesting);
}
/**
* Returns allowDanglingEdges
*/
public boolean isAllowDanglingEdges()
{
return allowDanglingEdges;
}
/**
* Sets allowDanglingEdges.
*/
public void setAllowDanglingEdges(boolean value)
{
boolean oldValue = allowDanglingEdges;
allowDanglingEdges = value;
changeSupport.firePropertyChange("allowDanglingEdges", oldValue,
allowDanglingEdges);
}
/**
* Returns cloneInvalidEdges.
*/
public boolean isCloneInvalidEdges()
{
return cloneInvalidEdges;
}
/**
* Sets cloneInvalidEdge.
*/
public void setCloneInvalidEdges(boolean value)
{
boolean oldValue = cloneInvalidEdges;
cloneInvalidEdges = value;
changeSupport.firePropertyChange("cloneInvalidEdges", oldValue,
cloneInvalidEdges);
}
/**
* Returns disconnectOnMove
*/
public boolean isDisconnectOnMove()
{
return disconnectOnMove;
}
/**
* Sets disconnectOnMove.
*/
public void setDisconnectOnMove(boolean value)
{
boolean oldValue = disconnectOnMove;
disconnectOnMove = value;
changeSupport.firePropertyChange("disconnectOnMove", oldValue,
disconnectOnMove);
}
/**
* Returns allowLoops.
*/
public boolean isAllowLoops()
{
return allowLoops;
}
/**
* Sets allowLoops.
*/
public void setAllowLoops(boolean value)
{
boolean oldValue = allowLoops;
allowLoops = value;
changeSupport.firePropertyChange("allowLoops", oldValue, allowLoops);
}
/**
* Returns connectableEdges.
*/
public boolean isConnectableEdges()
{
return connectableEdges;
}
/**
* Sets connetableEdges.
*/
public void setConnectableEdges(boolean value)
{
boolean oldValue = connectableEdges;
connectableEdges = value;
changeSupport.firePropertyChange("connectableEdges", oldValue,
connectableEdges);
}
/**
* Returns resetEdgesOnMove.
*/
public boolean isResetEdgesOnMove()
{
return resetEdgesOnMove;
}
/**
* Sets resetEdgesOnMove.
*/
public void setResetEdgesOnMove(boolean value)
{
boolean oldValue = resetEdgesOnMove;
resetEdgesOnMove = value;
changeSupport.firePropertyChange("resetEdgesOnMove", oldValue,
resetEdgesOnMove);
}
/**
* Returns resetViewOnRootChange.
*/
public boolean isResetViewOnRootChange()
{
return resetViewOnRootChange;
}
/**
* Sets resetEdgesOnResize.
*/
public void setResetViewOnRootChange(boolean value)
{
boolean oldValue = resetViewOnRootChange;
resetViewOnRootChange = value;
changeSupport.firePropertyChange("resetViewOnRootChange", oldValue,
resetViewOnRootChange);
}
/**
* Returns resetEdgesOnResize.
*/
public boolean isResetEdgesOnResize()
{
return resetEdgesOnResize;
}
/**
* Sets resetEdgesOnResize.
*/
public void setResetEdgesOnResize(boolean value)
{
boolean oldValue = resetEdgesOnResize;
resetEdgesOnResize = value;
changeSupport.firePropertyChange("resetEdgesOnResize", oldValue,
resetEdgesOnResize);
}
/**
* Returns resetEdgesOnConnect.
*/
public boolean isResetEdgesOnConnect()
{
return resetEdgesOnConnect;
}
/**
* Sets resetEdgesOnConnect.
*/
public void setResetEdgesOnConnect(boolean value)
{
boolean oldValue = resetEdgesOnConnect;
resetEdgesOnConnect = value;
changeSupport.firePropertyChange("resetEdgesOnConnect", oldValue,
resetEdgesOnResize);
}
/**
* Returns true if the size of the given cell should automatically be
* updated after a change of the label. This implementation returns
* autoSize for all given cells.
*
* @param cell Cell that should be resized.
* @return Returns true if the size of the given cell should be updated.
*/
public boolean isAutoSizeCell(Object cell)
{
return isAutoSizeCells();
}
/**
* Returns true if the size of the given cell should automatically be
* updated after a change of the label. This implementation returns
* autoSize for all given cells.
*/
public boolean isAutoSizeCells()
{
return autoSizeCells;
}
/**
* Specifies if cell sizes should be automatically updated after a label
* change. This implementation sets autoSize to the given parameter.
*
* @param value Boolean indicating if cells should be resized
* automatically.
*/
public void setAutoSizeCells(boolean value)
{
boolean oldValue = autoSizeCells;
autoSizeCells = value;
changeSupport.firePropertyChange("autoSizeCells", oldValue,
autoSizeCells);
}
/**
* Returns true if the parent of the given cell should be extended if the
* child has been resized so that it overlaps the parent. This
* implementation returns ExtendParents if cell is not an edge.
*
* @param cell Cell that has been resized.
*/
public boolean isExtendParent(Object cell)
{
return !getModel().isEdge(cell) && isExtendParents();
}
/**
* Returns extendParents.
*/
public boolean isExtendParents()
{
return extendParents;
}
/**
* Sets extendParents.
*/
public void setExtendParents(boolean value)
{
boolean oldValue = extendParents;
extendParents = value;
changeSupport.firePropertyChange("extendParents", oldValue,
extendParents);
}
/**
* Returns extendParentsOnAdd.
*/
public boolean isExtendParentsOnAdd()
{
return extendParentsOnAdd;
}
/**
* Sets extendParentsOnAdd.
*/
public void setExtendParentsOnAdd(boolean value)
{
boolean oldValue = extendParentsOnAdd;
extendParentsOnAdd = value;
changeSupport.firePropertyChange("extendParentsOnAdd", oldValue,
extendParentsOnAdd);
}
/**
* Returns true if the given cell should be kept inside the bounds of its
* parent according to the rules defined by getOverlap and
* isAllowOverlapParent. This implementation returns
* isConstrainChildren() for all given cells.
*/
public boolean isConstrainChild(Object cell)
{
return isConstrainChildren();
}
/**
* Returns constrainChildren.
*
* @return the keepInsideParentOnMove
*/
public boolean isConstrainChildren()
{
return constrainChildren;
}
/**
* @param value the constrainChildren to set
*/
public void setConstrainChildren(boolean value)
{
boolean oldValue = constrainChildren;
constrainChildren = value;
changeSupport.firePropertyChange("constrainChildren", oldValue,
constrainChildren);
}
/**
* Returns autoOrigin.
*/
public boolean isAutoOrigin()
{
return autoOrigin;
}
/**
* @param value the autoOrigin to set
*/
public void setAutoOrigin(boolean value)
{
boolean oldValue = autoOrigin;
autoOrigin = value;
changeSupport.firePropertyChange("autoOrigin", oldValue, autoOrigin);
}
/**
* Returns origin.
*/
public mxPoint getOrigin()
{
return origin;
}
/**
* @param value the origin to set
*/
public void setOrigin(mxPoint value)
{
mxPoint oldValue = origin;
origin = value;
changeSupport.firePropertyChange("origin", oldValue, origin);
}
/**
* @return Returns changesRepaintThreshold.
*/
public int getChangesRepaintThreshold()
{
return changesRepaintThreshold;
}
/**
* @param value the changesRepaintThreshold to set
*/
public void setChangesRepaintThreshold(int value)
{
int oldValue = changesRepaintThreshold;
changesRepaintThreshold = value;
changeSupport.firePropertyChange("changesRepaintThreshold", oldValue,
changesRepaintThreshold);
}
/**
* Returns isAllowNegativeCoordinates.
*
* @return the allowNegativeCoordinates
*/
public boolean isAllowNegativeCoordinates()
{
return allowNegativeCoordinates;
}
/**
* @param value the allowNegativeCoordinates to set
*/
public void setAllowNegativeCoordinates(boolean value)
{
boolean oldValue = allowNegativeCoordinates;
allowNegativeCoordinates = value;
changeSupport.firePropertyChange("allowNegativeCoordinates", oldValue,
allowNegativeCoordinates);
}
/**
* Returns collapseToPreferredSize.
*
* @return the collapseToPreferredSize
*/
public boolean isCollapseToPreferredSize()
{
return collapseToPreferredSize;
}
/**
* @param value the collapseToPreferredSize to set
*/
public void setCollapseToPreferredSize(boolean value)
{
boolean oldValue = collapseToPreferredSize;
collapseToPreferredSize = value;
changeSupport.firePropertyChange("collapseToPreferredSize", oldValue,
collapseToPreferredSize);
}
/**
* @return Returns true if edges are rendered in the foreground.
*/
public boolean isKeepEdgesInForeground()
{
return keepEdgesInForeground;
}
/**
* @param value the keepEdgesInForeground to set
*/
public void setKeepEdgesInForeground(boolean value)
{
boolean oldValue = keepEdgesInForeground;
keepEdgesInForeground = value;
changeSupport.firePropertyChange("keepEdgesInForeground", oldValue,
keepEdgesInForeground);
}
/**
* @return Returns true if edges are rendered in the background.
*/
public boolean isKeepEdgesInBackground()
{
return keepEdgesInBackground;
}
/**
* @param value the keepEdgesInBackground to set
*/
public void setKeepEdgesInBackground(boolean value)
{
boolean oldValue = keepEdgesInBackground;
keepEdgesInBackground = value;
changeSupport.firePropertyChange("keepEdgesInBackground", oldValue,
keepEdgesInBackground);
}
/**
* Returns true if the given cell is a valid source for new connections.
* This implementation returns true for all non-null values and is
* called by is called by <isValidConnection>.
*
* @param cell Object that represents a possible source or null.
* @return Returns true if the given cell is a valid source terminal.
*/
public boolean isValidSource(Object cell)
{
return (cell == null && allowDanglingEdges)
|| (cell != null
&& (!model.isEdge(cell) || isConnectableEdges()) && isCellConnectable(cell));
}
/**
* Returns isValidSource for the given cell. This is called by
* isValidConnection.
*
* @param cell Object that represents a possible target or null.
* @return Returns true if the given cell is a valid target.
*/
public boolean isValidTarget(Object cell)
{
return isValidSource(cell);
}
/**
* Returns true if the given target cell is a valid target for source.
* This is a boolean implementation for not allowing connections between
* certain pairs of vertices and is called by <getEdgeValidationError>.
* This implementation returns true if <isValidSource> returns true for
* the source and <isValidTarget> returns true for the target.
*
* @param source Object that represents the source cell.
* @param target Object that represents the target cell.
* @return Returns true if the the connection between the given terminals
* is valid.
*/
public boolean isValidConnection(Object source, Object target)
{
return isValidSource(source) && isValidTarget(target)
&& (isAllowLoops() || source != target);
}
/**
* Returns the minimum size of the diagram.
*
* @return Returns the minimum container size.
*/
public mxRectangle getMinimumGraphSize()
{
return minimumGraphSize;
}
/**
* @param value the minimumGraphSize to set
*/
public void setMinimumGraphSize(mxRectangle value)
{
mxRectangle oldValue = minimumGraphSize;
minimumGraphSize = value;
changeSupport.firePropertyChange("minimumGraphSize", oldValue, value);
}
/**
* Returns a decimal number representing the amount of the width and height
* of the given cell that is allowed to overlap its parent. A value of 0
* means all children must stay inside the parent, 1 means the child is
* allowed to be placed outside of the parent such that it touches one of
* the parents sides. If <isAllowOverlapParent> returns false for the given
* cell, then this method returns 0.
*
* @param cell
* @return Returns the overlapping value for the given cell inside its
* parent.
*/
public double getOverlap(Object cell)
{
return (isAllowOverlapParent(cell)) ? getDefaultOverlap() : 0;
}
/**
* Gets defaultOverlap.
*/
public double getDefaultOverlap()
{
return defaultOverlap;
}
/**
* Sets defaultOverlap.
*/
public void setDefaultOverlap(double value)
{
double oldValue = defaultOverlap;
defaultOverlap = value;
changeSupport.firePropertyChange("defaultOverlap", oldValue, value);
}
/**
* Returns true if the given cell is allowed to be placed outside of the
* parents area.
*
* @param cell
* @return Returns true if the given cell may overlap its parent.
*/
public boolean isAllowOverlapParent(Object cell)
{
return false;
}
/**
* Returns the cells which are movable in the given array of cells.
*/
public Object[] getFoldableCells(Object[] cells, final boolean collapse)
{
return mxGraphModel.filterCells(cells, new Filter()
{
public boolean filter(Object cell)
{
return isCellFoldable(cell, collapse);
}
});
}
/**
* Returns true if the given cell is expandable. This implementation
* returns true if the cell has at least one child.
*
* @param cell <mxCell> whose expandable state should be returned.
* @return Returns true if the given cell is expandable.
*/
public boolean isCellFoldable(Object cell, boolean collapse)
{
return model.getChildCount(cell) > 0;
}
/**
* Returns true if the grid is enabled.
*
* @return Returns the enabled state of the grid.
*/
public boolean isGridEnabled()
{
return gridEnabled;
}
/**
* Sets if the grid is enabled.
*
* @param value Specifies if the grid should be enabled.
*/
public void setGridEnabled(boolean value)
{
boolean oldValue = gridEnabled;
gridEnabled = value;
changeSupport.firePropertyChange("gridEnabled", oldValue, gridSize);
}
/**
* Returns the grid size.
*
* @return Returns the grid size
*/
public int getGridSize()
{
return gridSize;
}
/**
* Sets the grid size and fires a property change event for gridSize.
*
* @param value New grid size to be used.
*/
public void setGridSize(int value)
{
int oldValue = gridSize;
gridSize = value;
changeSupport.firePropertyChange("gridSize", oldValue, gridSize);
}
/**
* Returns alternateEdgeStyle.
*/
public String getAlternateEdgeStyle()
{
return alternateEdgeStyle;
}
/**
* Sets alternateEdgeStyle.
*/
public void setAlternateEdgeStyle(String value)
{
String oldValue = alternateEdgeStyle;
alternateEdgeStyle = value;
changeSupport.firePropertyChange("alternateEdgeStyle", oldValue,
alternateEdgeStyle);
}
/**
* Returns true if the given cell is a valid drop target for the specified
* cells. This returns true if the cell is a swimlane, has children and is
* not collapsed, or if splitEnabled is true and isSplitTarget returns
* true for the given arguments
*
* @param cell Object that represents the possible drop target.
* @param cells Objects that are going to be dropped.
* @return Returns true if the cell is a valid drop target for the given
* cells.
*/
public boolean isValidDropTarget(Object cell, Object[] cells)
{
return cell != null
&& ((isSplitEnabled() && isSplitTarget(cell, cells)) || (!model
.isEdge(cell) && (isSwimlane(cell) || (model
.getChildCount(cell) > 0 && !isCellCollapsed(cell)))));
}
/**
* Returns true if split is enabled and the given edge may be splitted into
* two edges with the given cell as a new terminal between the two.
*
* @param target Object that represents the edge to be splitted.
* @param cells Array of cells to add into the given edge.
* @return Returns true if the given edge may be splitted by the given
* cell.
*/
public boolean isSplitTarget(Object target, Object[] cells)
{
if (target != null && cells != null && cells.length == 1)
{
Object src = model.getTerminal(target, true);
Object trg = model.getTerminal(target, false);
return (model.isEdge(target)
&& isCellConnectable(cells[0])
&& getEdgeValidationError(target,
model.getTerminal(target, true), cells[0]) == null
&& !model.isAncestor(cells[0], src) && !model.isAncestor(
cells[0], trg));
}
return false;
}
/**
* Returns the given cell if it is a drop target for the given cells or the
* nearest ancestor that may be used as a drop target for the given cells.
* If the given array contains a swimlane and swimlaneNesting is false
* then this always returns null. If no cell is given, then the bottommost
* swimlane at the location of the given event is returned.
*
* This function should only be used if isDropEnabled returns true.
*/
public Object getDropTarget(Object[] cells, Point pt, Object cell)
{
if (!isSwimlaneNesting())
{
for (int i = 0; i < cells.length; i++)
{
if (isSwimlane(cells[i]))
{
return null;
}
}
}
// FIXME the else below does nothing if swimlane is null
Object swimlane = null; //getSwimlaneAt(pt.x, pt.y);
if (cell == null)
{
cell = swimlane;
}
else if (swimlane != null)
{
// Checks if the cell is an ancestor of the swimlane
// under the mouse and uses the swimlane in that case
Object tmp = model.getParent(swimlane);
while (tmp != null && isSwimlane(tmp) && tmp != cell)
{
tmp = model.getParent(tmp);
}
if (tmp == cell)
{
cell = swimlane;
}
}
while (cell != null && !isValidDropTarget(cell, cells)
&& model.getParent(cell) != model.getRoot())
{
cell = model.getParent(cell);
}
return (model.getParent(cell) != model.getRoot() && !mxUtils.contains(
cells, cell)) ? cell : null;
};
//
// Cell retrieval
//
/**
* Returns the first child of the root in the model, that is, the first or
* default layer of the diagram.
*
* @return Returns the default parent for new cells.
*/
public Object getDefaultParent()
{
Object parent = defaultParent;
if (parent == null)
{
parent = view.getCurrentRoot();
if (parent == null)
{
Object root = model.getRoot();
parent = model.getChildAt(root, 0);
}
}
return parent;
}
/**
* Sets the default parent to be returned by getDefaultParent.
* Set this to null to return the first child of the root in
* getDefaultParent.
*/
public void setDefaultParent(Object value)
{
defaultParent = value;
}
/**
* Returns the visible child vertices of the given parent.
*
* @param parent Cell whose children should be returned.
*/
public Object[] getChildVertices(Object parent)
{
return getChildCells(parent, true, false);
}
/**
* Returns the visible child edges of the given parent.
*
* @param parent Cell whose children should be returned.
*/
public Object[] getChildEdges(Object parent)
{
return getChildCells(parent, false, true);
}
/**
* Returns the visible children of the given parent.
*
* @param parent Cell whose children should be returned.
*/
public Object[] getChildCells(Object parent)
{
return getChildCells(parent, false, false);
}
/**
* Returns the visible child vertices or edges in the given parent. If
* vertices and edges is false, then all children are returned.
*
* @param parent Cell whose children should be returned.
* @param vertices Specifies if child vertices should be returned.
* @param edges Specifies if child edges should be returned.
* @return Returns the child vertices and edges.
*/
public Object[] getChildCells(Object parent, boolean vertices, boolean edges)
{
Object[] cells = mxGraphModel.getChildCells(model, parent, vertices,
edges);
List<Object> result = new ArrayList<Object>(cells.length);
// Filters out the non-visible child cells
for (int i = 0; i < cells.length; i++)
{
if (isCellVisible(cells[i]))
{
result.add(cells[i]);
}
}
return result.toArray();
}
/**
* Returns all visible edges connected to the given cell without loops.
*
* @param cell Cell whose connections should be returned.
* @return Returns the connected edges for the given cell.
*/
public Object[] getConnections(Object cell)
{
return getConnections(cell, null);
}
/**
* Returns all visible edges connected to the given cell without loops.
* If the optional parent argument is specified, then only child
* edges of the given parent are returned.
*
* @param cell Cell whose connections should be returned.
* @param parent Optional parent of the opposite end for a connection
* to be returned.
* @return Returns the connected edges for the given cell.
*/
public Object[] getConnections(Object cell, Object parent)
{
return getEdges(cell, parent, true, true, false);
}
/**
* Returns all incoming visible edges connected to the given cell without
* loops.
*
* @param cell Cell whose incoming edges should be returned.
* @return Returns the incoming edges of the given cell.
*/
public Object[] getIncomingEdges(Object cell)
{
return getIncomingEdges(cell, null);
}
/**
* Returns the visible incoming edges for the given cell. If the optional
* parent argument is specified, then only child edges of the given parent
* are returned.
*
* @param cell Cell whose incoming edges should be returned.
* @param parent Optional parent of the opposite end for an edge
* to be returned.
* @return Returns the incoming edges of the given cell.
*/
public Object[] getIncomingEdges(Object cell, Object parent)
{
return getEdges(cell, parent, true, false, false);
}
/**
* Returns all outgoing visible edges connected to the given cell without
* loops.
*
* @param cell Cell whose outgoing edges should be returned.
* @return Returns the outgoing edges of the given cell.
*/
public Object[] getOutgoingEdges(Object cell)
{
return getOutgoingEdges(cell, null);
}
/**
* Returns the visible outgoing edges for the given cell. If the optional
* parent argument is specified, then only child edges of the given parent
* are returned.
*
* @param cell Cell whose outgoing edges should be returned.
* @param parent Optional parent of the opposite end for an edge
* to be returned.
* @return Returns the outgoing edges of the given cell.
*/
public Object[] getOutgoingEdges(Object cell, Object parent)
{
return getEdges(cell, parent, false, true, false);
}
/**
* Returns all visible edges connected to the given cell including loops.
*
* @param cell Cell whose edges should be returned.
* @return Returns the edges of the given cell.
*/
public Object[] getEdges(Object cell)
{
return getEdges(cell, null);
}
/**
* Returns all visible edges connected to the given cell including loops.
*
* @param cell Cell whose edges should be returned.
* @param parent Optional parent of the opposite end for an edge
* to be returned.
* @return Returns the edges of the given cell.
*/
public Object[] getEdges(Object cell, Object parent)
{
return getEdges(cell, parent, true, true, true);
}
/**
* Returns the incoming and/or outgoing edges for the given cell.
* If the optional parent argument is specified, then only edges are returned
* where the opposite is in the given parent cell.
*
* @param cell Cell whose edges should be returned.
* @param parent Optional parent of the opposite end for an edge to be
* returned.
* @param incoming Specifies if incoming edges should be included in the
* result.
* @param outgoing Specifies if outgoing edges should be included in the
* result.
* @param includeLoops Specifies if loops should be included in the result.
* @return Returns the edges connected to the given cell.
*/
public Object[] getEdges(Object cell, Object parent, boolean incoming,
boolean outgoing, boolean includeLoops)
{
boolean isCollapsed = isCellCollapsed(cell);
List<Object> edges = new ArrayList<Object>();
int childCount = model.getChildCount(cell);
for (int i = 0; i < childCount; i++)
{
Object child = model.getChildAt(cell, i);
if (isCollapsed || !isCellVisible(child))
{
edges.addAll(Arrays.asList(mxGraphModel.getEdges(model, child,
incoming, outgoing, includeLoops)));
}
}
edges.addAll(Arrays.asList(mxGraphModel.getEdges(model, cell, incoming,
outgoing, includeLoops)));
List<Object> result = new ArrayList<Object>(edges.size());
Iterator<Object> it = edges.iterator();
while (it.hasNext())
{
Object edge = it.next();
Object source = view.getVisibleTerminal(edge, true);
Object target = view.getVisibleTerminal(edge, false);
if (includeLoops
|| ((source != target) && ((incoming && target == cell && (parent == null || model
.getParent(source) == parent)) || (outgoing
&& source == cell && (parent == null || model
.getParent(target) == parent)))))
{
result.add(edge);
}
}
return result.toArray();
}
/**
* Returns all distinct visible opposite cells of the terminal on the given
* edges.
*
* @param edges
* @param terminal
* @return Returns the terminals at the opposite ends of the given edges.
*/
public Object[] getOpposites(Object[] edges, Object terminal)
{
return getOpposites(edges, terminal, true, true);
}
/**
* Returns all distincts visible opposite cells for the specified terminal
* on the given edges.
*
* @param edges Edges whose opposite terminals should be returned.
* @param terminal Terminal that specifies the end whose opposite should be
* returned.
* @param sources Specifies if source terminals should be included in the
* result.
* @param targets Specifies if targer terminals should be included in the
* result.
* @return Returns the cells at the oppsite ends of the given edges.
*/
public Object[] getOpposites(Object[] edges, Object terminal,
boolean sources, boolean targets)
{
Collection<Object> terminals = new LinkedHashSet<Object>();
if (edges != null)
{
for (int i = 0; i < edges.length; i++)
{
Object source = view.getVisibleTerminal(edges[i], true);
Object target = view.getVisibleTerminal(edges[i], false);
// Checks if the terminal is the source of
// the edge and if the target should be
// stored in the result
if (targets && source == terminal && target != null
&& target != terminal)
{
terminals.add(target);
}
// Checks if the terminal is the taget of
// the edge and if the source should be
// stored in the result
else if (sources && target == terminal && source != null
&& source != terminal)
{
terminals.add(source);
}
}
}
return terminals.toArray();
}
/**
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and returns the connected edges
* as displayed on the screen.
*
* @param source
* @param target
* @return Returns all edges between the given terminals.
*/
public Object[] getEdgesBetween(Object source, Object target)
{
return getEdgesBetween(source, target, false);
}
/**
* Returns the edges between the given source and target. This takes into
* account collapsed and invisible cells and returns the connected edges
* as displayed on the screen.
*
* @param source
* @param target
* @param directed
* @return Returns all edges between the given terminals.
*/
public Object[] getEdgesBetween(Object source, Object target,
boolean directed)
{
Object[] edges = getEdges(source);
List<Object> result = new ArrayList<Object>(edges.length);
// Checks if the edge is connected to the correct
// cell and adds any match to the result
for (int i = 0; i < edges.length; i++)
{
Object src = view.getVisibleTerminal(edges[i], true);
Object trg = view.getVisibleTerminal(edges[i], false);
if ((src == source && trg == target)
|| (!directed && src == target && trg == source))
{
result.add(edges[i]);
}
}
return result.toArray();
}
/**
* Returns the children of the given parent that are contained in the
* halfpane from the given point (x0, y0) rightwards and downwards
* depending on rightHalfpane and bottomHalfpane.
*
* @param x0 X-coordinate of the origin.
* @param y0 Y-coordinate of the origin.
* @param parent <mxCell> whose children should be checked.
* @param rightHalfpane Boolean indicating if the cells in the right halfpane
* from the origin should be returned.
* @param bottomHalfpane Boolean indicating if the cells in the bottom halfpane
* from the origin should be returned.
* @return Returns the cells beyond the given halfpane.
*/
public Object[] getCellsBeyond(double x0, double y0, Object parent,
boolean rightHalfpane, boolean bottomHalfpane)
{
if (parent == null)
{
parent = getDefaultParent();
}
int childCount = model.getChildCount(parent);
List<Object> result = new ArrayList<Object>(childCount);
if (rightHalfpane || bottomHalfpane)
{
if (parent != null)
{
for (int i = 0; i < childCount; i++)
{
Object child = model.getChildAt(parent, i);
mxCellState state = view.getState(child);
if (isCellVisible(child) && state != null)
{
if ((!rightHalfpane || state.getX() >= x0)
&& (!bottomHalfpane || state.getY() >= y0))
{
result.add(child);
}
}
}
}
}
return result.toArray();
}
/**
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the with the greatest
* difference between incoming and outgoing edges is returned. This
* takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* @param parent Cell whose children should be checked.
* @return Array of tree roots in parent.
*/
public Object[] findTreeRoots(Object parent)
{
return findTreeRoots(parent, false);
}
/**
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* @param parent Cell whose children should be checked.
* @param isolate Specifies if edges should be ignored if the opposite
* end is not a child of the given parent cell.
* @return Array of tree roots in parent.
*/
public Object[] findTreeRoots(Object parent, boolean isolate)
{
return findTreeRoots(parent, isolate, false);
}
/**
* Returns all visible children in the given parent which do not have
* incoming edges. If the result is empty then the children with the
* maximum difference between incoming and outgoing edges are returned.
* This takes into account edges that are being promoted to the given
* root due to invisible children or collapsed cells.
*
* @param parent Cell whose children should be checked.
* @param isolate Specifies if edges should be ignored if the opposite
* end is not a child of the given parent cell.
* @param invert Specifies if outgoing or incoming edges should be counted
* for a tree root. If false then outgoing edges will be counted.
* @return Array of tree roots in parent.
*/
public Object[] findTreeRoots(Object parent, boolean isolate, boolean invert)
{
List<Object> roots = new ArrayList<Object>();
if (parent != null)
{
int childCount = model.getChildCount(parent);
Object best = null;
int maxDiff = 0;
for (int i = 0; i < childCount; i++)
{
Object cell = model.getChildAt(parent, i);
if (model.isVertex(cell) && isCellVisible(cell))
{
Object[] conns = getConnections(cell, (isolate) ? parent
: null);
int fanOut = 0;
int fanIn = 0;
for (int j = 0; j < conns.length; j++)
{
Object src = view.getVisibleTerminal(conns[j], true);
if (src == cell)
{
fanOut++;
}
else
{
fanIn++;
}
}
if ((invert && fanOut == 0 && fanIn >= 0)
|| (!invert && fanIn == 0 && fanOut >= 0))
{
roots.add(cell);
}
int diff = (invert) ? fanIn - fanOut : fanOut - fanIn;
if (diff >= maxDiff)
{
maxDiff = diff;
best = cell;
if (!roots.contains(cell))
roots.add(cell);
}
}
}
}
return roots.toArray();
}
/**
* Traverses the tree starting at the given vertex. Here is how to use this
* method for a given vertex (root) which is typically the root of a tree:
* <code>
* graph.traverse(root, true, new mxICellVisitor()
* {
* public boolean visit(Object vertex, Object edge)
* {
* System.out.println("edge="+graph.convertValueToString(edge)+
* " vertex="+graph.convertValueToString(vertex));
*
* return true;
* }
* });
* </code>
*
* @param vertex
* @param directed
* @param visitor
*/
public void traverse(Object vertex, boolean directed, mxICellVisitor visitor)
{
traverse(vertex, directed, visitor, null, null);
}
/**
* Traverses the (directed) graph invoking the given function for each
* visited vertex and edge. The function is invoked with the current vertex
* and the incoming edge as a parameter. This implementation makes sure
* each vertex is only visited once. The function may return false if the
* traversal should stop at the given vertex.
*
* @param vertex <mxCell> that represents the vertex where the traversal starts.
* @param directed Optional boolean indicating if edges should only be traversed
* from source to target. Default is true.
* @param visitor Visitor that takes the current vertex and the incoming edge.
* The traversal stops if the function returns false.
* @param edge Optional <mxCell> that represents the incoming edge. This is
* null for the first step of the traversal.
* @param visited Optional array of cell paths for the visited cells.
*/
public void traverse(Object vertex, boolean directed,
mxICellVisitor visitor, Object edge, Set<Object> visited)
{
if (vertex != null && visitor != null)
{
if (visited == null)
{
visited = new HashSet<Object>();
}
if (!visited.contains(vertex))
{
visited.add(vertex);
if (visitor.visit(vertex, edge))
{
int edgeCount = model.getEdgeCount(vertex);
if (edgeCount > 0)
{
for (int i = 0; i < edgeCount; i++)
{
Object e = model.getEdgeAt(vertex, i);
boolean isSource = model.getTerminal(e, true) == vertex;
if (!directed || isSource)
{
Object next = model.getTerminal(e, !isSource);
traverse(next, directed, visitor, e, visited);
}
}
}
}
}
}
}
//
// Selection
//
/**
*
*/
public mxGraphSelectionModel getSelectionModel()
{
return selectionModel;
}
/**
*
*/
public int getSelectionCount()
{
return selectionModel.size();
}
/**
*
* @param cell
* @return Returns true if the given cell is selected.
*/
public boolean isCellSelected(Object cell)
{
return selectionModel.isSelected(cell);
}
/**
*
* @return Returns true if the selection is empty.
*/
public boolean isSelectionEmpty()
{
return selectionModel.isEmpty();
}
/**
*
*/
public void clearSelection()
{
selectionModel.clear();
}
/**
*
* @return Returns the selection cell.
*/
public Object getSelectionCell()
{
return selectionModel.getCell();
}
/**
*
* @param cell
*/
public void setSelectionCell(Object cell)
{
selectionModel.setCell(cell);
}
/**
*
* @return Returns the selection cells.
*/
public Object[] getSelectionCells()
{
return selectionModel.getCells();
}
/**
*
*/
public void setSelectionCells(Object[] cells)
{
selectionModel.setCells(cells);
}
/**
*
* @param cells
*/
public void setSelectionCells(Collection<Object> cells)
{
if (cells != null)
{
setSelectionCells(cells.toArray());
}
}
/**
*
*/
public void addSelectionCell(Object cell)
{
selectionModel.addCell(cell);
}
/**
*
*/
public void addSelectionCells(Object[] cells)
{
selectionModel.addCells(cells);
}
/**
*
*/
public void removeSelectionCell(Object cell)
{
selectionModel.removeCell(cell);
}
/**
*
*/
public void removeSelectionCells(Object[] cells)
{
selectionModel.removeCells(cells);
}
/**
* Selects the next cell.
*/
public void selectNextCell()
{
selectCell(true, false, false);
}
/**
* Selects the previous cell.
*/
public void selectPreviousCell()
{
selectCell(false, false, false);
}
/**
* Selects the parent cell.
*/
public void selectParentCell()
{
selectCell(false, true, false);
}
/**
* Selects the first child cell.
*/
public void selectChildCell()
{
selectCell(false, false, true);
}
/**
* Selects the next, parent, first child or previous cell, if all arguments
* are false.
*
* @param isNext
* @param isParent
* @param isChild
*/
public void selectCell(boolean isNext, boolean isParent, boolean isChild)
{
Object cell = getSelectionCell();
if (getSelectionCount() > 1)
{
clearSelection();
}
Object parent = (cell != null) ? model.getParent(cell)
: getDefaultParent();
int childCount = model.getChildCount(parent);
if (cell == null && childCount > 0)
{
Object child = model.getChildAt(parent, 0);
setSelectionCell(child);
}
else if ((cell == null || isParent) && view.getState(parent) != null
&& model.getGeometry(parent) != null)
{
if (getCurrentRoot() != parent)
{
setSelectionCell(parent);
}
}
else if (cell != null && isChild)
{
int tmp = model.getChildCount(cell);
if (tmp > 0)
{
Object child = model.getChildAt(cell, 0);
setSelectionCell(child);
}
}
else if (childCount > 0)
{
int i = ((mxICell) parent).getIndex((mxICell) cell);
if (isNext)
{
i++;
setSelectionCell(model.getChildAt(parent, i % childCount));
}
else
{
i--;
int index = (i < 0) ? childCount - 1 : i;
setSelectionCell(model.getChildAt(parent, index));
}
}
}
/**
* Selects all vertices inside the default parent.
*/
public void selectVertices()
{
selectVertices(null);
}
/**
* Selects all vertices inside the given parent or the default parent
* if no parent is given.
*/
public void selectVertices(Object parent)
{
selectCells(true, false, parent);
}
/**
* Selects all vertices inside the default parent.
*/
public void selectEdges()
{
selectEdges(null);
}
/**
* Selects all vertices inside the given parent or the default parent
* if no parent is given.
*/
public void selectEdges(Object parent)
{
selectCells(false, true, parent);
}
/**
* Selects all vertices and/or edges depending on the given boolean
* arguments recursively, starting at the default parent. Use
* <code>selectAll</code> to select all cells.
*
* @param vertices Boolean indicating if vertices should be selected.
* @param edges Boolean indicating if edges should be selected.
*/
public void selectCells(boolean vertices, boolean edges)
{
selectCells(vertices, edges, null);
}
/**
* Selects all vertices and/or edges depending on the given boolean
* arguments recursively, starting at the given parent or the default
* parent if no parent is specified. Use <code>selectAll</code> to select
* all cells.
*
* @param vertices Boolean indicating if vertices should be selected.
* @param edges Boolean indicating if edges should be selected.
* @param parent Optional cell that acts as the root of the recursion.
* Default is <code>defaultParent</code>.
*/
public void selectCells(final boolean vertices, final boolean edges,
Object parent)
{
if (parent == null)
{
parent = getDefaultParent();
}
Collection<Object> cells = mxGraphModel.filterDescendants(getModel(),
new mxGraphModel.Filter()
{
/**
*
*/
public boolean filter(Object cell)
{
return view.getState(cell) != null
&& model.getChildCount(cell) == 0
&& ((model.isVertex(cell) && vertices) || (model
.isEdge(cell) && edges));
}
});
setSelectionCells(cells);
}
/**
*
*/
public void selectAll()
{
selectAll(null);
}
/**
* Selects all children of the given parent cell or the children of the
* default parent if no parent is specified. To select leaf vertices and/or
* edges use <selectCells>.
*
* @param parent Optional <mxCell> whose children should be selected.
* Default is <defaultParent>.
*/
public void selectAll(Object parent)
{
if (parent == null)
{
parent = getDefaultParent();
}
Object[] children = mxGraphModel.getChildren(model, parent);
if (children != null)
{
setSelectionCells(children);
}
}
//
// Images and drawing
//
/**
* Draws the graph onto the given canvas.
*
* @param canvas Canvas onto which the graph should be drawn.
*/
public void drawGraph(mxICanvas canvas)
{
drawCell(canvas, getModel().getRoot());
}
/**
* Draws the given cell and its descendants onto the specified canvas.
*
* @param canvas Canvas onto which the cell should be drawn.
* @param cell Cell that should be drawn onto the canvas.
*/
public void drawCell(mxICanvas canvas, Object cell)
{
drawState(canvas, getView().getState(cell), getLabel(cell));
// Draws the children on top of their parent
int childCount = model.getChildCount(cell);
for (int i = 0; i < childCount; i++)
{
Object child = model.getChildAt(cell, i);
drawCell(canvas, child);
}
}
/**
* Draws the cell state with the given label onto the canvas. No
* children or descendants are painted here. This method invokes
* cellDrawn after the cell, but not its descendants have been
* painted.
*
* @param canvas Canvas onto which the cell should be drawn.
* @param state State of the cell to be drawn.
* @param label Label of the cell to be dranw.
*/
public void drawState(mxICanvas canvas, mxCellState state, String label)
{
Object cell = (state != null) ? state.getCell() : null;
if (cell != null && cell != view.getCurrentRoot()
&& cell != model.getRoot()
&& (model.isVertex(cell) || model.isEdge(cell)))
{
Object obj = canvas.drawCell(state);
Object lab = null;
// Holds the current clipping region in case the label will
// be clipped
Shape clip = null;
Rectangle newClip = state.getRectangle();
// Indirection for image canvas that contains a graphics canvas
mxICanvas clippedCanvas = (isLabelClipped(state.getCell())) ? canvas
: null;
if (clippedCanvas instanceof mxImageCanvas)
{
clippedCanvas = ((mxImageCanvas) clippedCanvas)
.getGraphicsCanvas();
// TODO: Shift newClip to match the image offset
//Point pt = ((mxImageCanvas) canvas).getTranslate();
//newClip.translate(-pt.x, -pt.y);
}
if (clippedCanvas instanceof mxGraphics2DCanvas)
{
Graphics g = ((mxGraphics2DCanvas) clippedCanvas).getGraphics();
clip = g.getClip();
g.setClip(newClip);
}
if (label != null && state.getLabelBounds() != null)
{
lab = canvas.drawLabel(label, state, isHtmlLabel(cell));
}
// Restores the previous clipping region
if (clippedCanvas instanceof mxGraphics2DCanvas)
{
((mxGraphics2DCanvas) clippedCanvas).getGraphics()
.setClip(clip);
}
// Invokes the cellDrawn callback with the object which was created
// by the canvas to represent the cell graphically
if (obj != null)
{
cellDrawn(canvas, state, obj, lab);
}
}
}
/**
* Called when a cell has been painted as the specified object, typically a
* DOM node that represents the given cell graphically in a document.
*/
protected void cellDrawn(mxICanvas canvas, mxCellState state,
Object element, Object labelElement)
{
if (element instanceof Element)
{
String link = getLinkForCell(state.getCell());
if (link != null)
{
String title = getToolTipForCell(state.getCell());
Element elem = (Element) element;
if (elem.getNodeName().startsWith("v:"))
{
elem.setAttribute("href", link.toString());
if (title != null)
{
elem.setAttribute("title", title);
}
}
else if (elem.getOwnerDocument().getElementsByTagName("svg")
.getLength() > 0)
{
Element xlink = elem.getOwnerDocument().createElement("a");
xlink.setAttribute("xlink:href", link.toString());
elem.getParentNode().replaceChild(xlink, elem);
xlink.appendChild(elem);
if (title != null)
{
xlink.setAttribute("xlink:title", title);
}
elem = xlink;
}
else
{
Element a = elem.getOwnerDocument().createElement("a");
a.setAttribute("href", link.toString());
a.setAttribute("style", "text-decoration:none;");
elem.getParentNode().replaceChild(a, elem);
a.appendChild(elem);
if (title != null)
{
a.setAttribute("title", title);
}
elem = a;
}
String target = getTargetForCell(state.getCell());
if (target != null)
{
elem.setAttribute("target", target);
}
}
}
}
/**
* Returns the hyperlink to be used for the given cell.
*/
protected String getLinkForCell(Object cell)
{
return null;
}
/**
* Returns the hyperlink to be used for the given cell.
*/
protected String getTargetForCell(Object cell)
{
return null;
}
//
// Redirected to change support
//
/**
* @param listener
* @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
*/
public void addPropertyChangeListener(PropertyChangeListener listener)
{
changeSupport.addPropertyChangeListener(listener);
}
/**
* @param propertyName
* @param listener
* @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
*/
public void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener)
{
changeSupport.addPropertyChangeListener(propertyName, listener);
}
/**
* @param listener
* @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
*/
public void removePropertyChangeListener(PropertyChangeListener listener)
{
changeSupport.removePropertyChangeListener(listener);
}
/**
* @param propertyName
* @param listener
* @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
*/
public void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener)
{
changeSupport.removePropertyChangeListener(propertyName, listener);
}
/**
* Prints the version number on the console.
*/
public static void main(String[] args)
{
System.out.println("mxGraph version \"" + VERSION + "\"");
}
}